billingengine-mcp 0.1.0 → 0.1.2

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
@@ -1,19 +1,21 @@
1
1
  # BillingEngine MCP server
2
2
 
3
+ [![AllMCPs Verified](https://allmcps.com/api/badge/billingengine-mcp)](https://allmcps.com/mcp/billingengine-mcp?verify=e69e154f-3c18-4a25-b8a9-42bfcd9c6182)
4
+
3
5
  Create customers and invoices, look up open invoices and record payments in
4
6
  [BillingEngine](https://www.billingengine.com) by talking to Claude, Cursor or any other
5
7
  [MCP](https://modelcontextprotocol.io) client:
6
8
 
7
- > "Create an invoice for Malermeister Müller over 100 euros."
9
+ > "Create an invoice for Acme Painting over 100 euros."
8
10
  >
9
11
  > "Which invoices are overdue?"
10
12
  >
11
- > "Müller paid invoice 2610030028, record the payment."
13
+ > "Acme paid invoice 2610030028, record the payment."
12
14
 
13
15
  BillingEngine is invoicing software for freelancers and small businesses with
14
- [e-invoicing](https://www.billingengine.com/en/features) (XRechnung and ZUGFeRD / Factur-X),
15
- DATEV export and a German and English interface. The server is a thin layer over the
16
- [BillingEngine API](https://www.billingengine.com/en/api) and runs on your computer.
16
+ [e-invoicing](https://www.billingengine.com/en/features) (EN 16931, XRechnung, ZUGFeRD / Factur-X).
17
+ The server is a thin layer over the [BillingEngine API](https://www.billingengine.com/en/api)
18
+ and runs on your computer.
17
19
 
18
20
  ## Requirements
19
21
 
package/dist/index.js CHANGED
@@ -20,7 +20,7 @@ const INSTRUCTIONS = [
20
20
  "Invoices are created as drafts and are not sent: tell the user to review and send them in BillingEngine, using the app_url in the result.",
21
21
  "Nothing can be deleted through this server.",
22
22
  ].join(" ");
23
- const server = new McpServer({ name: "billingengine", version: "0.1.0" }, { instructions: INSTRUCTIONS });
23
+ const server = new McpServer({ name: "billingengine", version: "0.1.2" }, { instructions: INSTRUCTIONS });
24
24
  const date = z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Use YYYY-MM-DD");
25
25
  const page = z.number().int().min(1).optional().describe("Page of the list, starting at 1");
26
26
  const address = z.object({
@@ -51,17 +51,20 @@ const item = z.object({
51
51
  const invoiceFields = {
52
52
  date: date.optional().describe("Invoice date, defaults to today"),
53
53
  due_days: z.number().int().min(0).optional().describe("Days until payment is due"),
54
- introduction: z.string().optional(),
55
- signature: z.string().optional(),
54
+ opening_text: z.string().optional().describe("Text above the items, defaults to the account's text"),
55
+ closing_text: z.string().optional().describe("Text below the items, defaults to the account's text"),
56
56
  service_period_start_date: date.optional(),
57
57
  service_period_end_date: date.optional(),
58
58
  };
59
59
  const READ = { readOnlyHint: true, openWorldHint: false };
60
60
  const WRITE = { readOnlyHint: false, destructiveHint: false, openWorldHint: false };
61
+ const UPDATE = { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false };
62
+ const output = z.looseObject({});
61
63
  async function respond(action) {
62
64
  try {
63
65
  const value = await action();
64
- return { content: [{ type: "text", text: typeof value === "string" ? value : JSON.stringify(value, null, 2) }] };
66
+ const structured = typeof value === "string" ? { message: value } : value;
67
+ return { content: [{ type: "text", text: JSON.stringify(structured, null, 2) }], structuredContent: structured };
65
68
  }
66
69
  catch (error) {
67
70
  const message = error instanceof BillingEngineError ? error.message : `Unexpected error: ${error.message}`;
@@ -81,32 +84,37 @@ server.registerTool("find_customers", {
81
84
  title: "Find customers",
82
85
  description: "Lists customers, optionally filtered by a search text that matches company, names and email.",
83
86
  inputSchema: { query: z.string().optional(), page },
87
+ outputSchema: output,
84
88
  annotations: READ,
85
89
  }, ({ query, page }) => respond(() => client.get("/customers", { query, page })));
86
90
  server.registerTool("create_customer", {
87
91
  title: "Create a customer",
88
92
  description: "Creates a customer. Needs a company or a last name and a full address (line 1, postal code, city, country).",
89
93
  inputSchema: { ...customerFields, address },
94
+ outputSchema: output,
90
95
  annotations: WRITE,
91
96
  }, ({ address, ...fields }) => respond(() => client.post("/customers", { ...fields, address_attributes: address })));
92
97
  server.registerTool("update_customer", {
93
98
  title: "Update a customer",
94
99
  description: "Changes the given fields of a customer; fields that are left out stay as they are.",
95
100
  inputSchema: { id: z.number().int(), ...customerFields, address: address.partial().optional() },
96
- annotations: { ...WRITE, idempotentHint: true },
101
+ outputSchema: output,
102
+ annotations: UPDATE,
97
103
  }, ({ id, address, ...fields }) => respond(() => client.patch(`/customers/${id}`, { ...fields, ...(address && { address_attributes: address }) })));
98
104
  server.registerTool("create_invoice", {
99
105
  title: "Create a draft invoice",
100
106
  description: "Creates a draft invoice for a customer. Number, language, texts and VAT treatment come from the account settings. " +
101
107
  "The invoice is not sent; it counts towards the monthly invoice maximum of the plan.",
102
108
  inputSchema: { customer_id: z.number().int(), items: z.array(item).min(1), ...invoiceFields },
109
+ outputSchema: output,
103
110
  annotations: WRITE,
104
111
  }, (input) => respond(async () => withInvoiceLinks(await client.post("/invoices", input))));
105
112
  server.registerTool("update_invoice", {
106
113
  title: "Update a draft invoice",
107
114
  description: "Changes a draft invoice. Passing items replaces all existing items, so send the complete list. Sent invoices cannot be changed.",
108
115
  inputSchema: { id: z.number().int(), items: z.array(item).min(1).optional(), ...invoiceFields },
109
- annotations: { ...WRITE, idempotentHint: true },
116
+ outputSchema: output,
117
+ annotations: UPDATE,
110
118
  }, ({ id, ...changes }) => respond(async () => withInvoiceLinks(await client.patch(`/invoices/${id}`, changes))));
111
119
  server.registerTool("list_invoices", {
112
120
  title: "List invoices",
@@ -120,12 +128,14 @@ server.registerTool("list_invoices", {
120
128
  page,
121
129
  per_page: z.number().int().min(1).max(100).optional(),
122
130
  },
131
+ outputSchema: output,
123
132
  annotations: READ,
124
133
  }, (filters) => respond(async () => withInvoiceLinks(await client.get("/invoices", filters))));
125
134
  server.registerTool("get_invoice", {
126
135
  title: "Get an invoice",
127
136
  description: "Returns one invoice with its items, amounts and payment status.",
128
137
  inputSchema: { id: z.number().int() },
138
+ outputSchema: output,
129
139
  annotations: READ,
130
140
  }, ({ id }) => respond(async () => withInvoiceLinks(await client.get(`/invoices/${id}`))));
131
141
  server.registerTool("download_invoice", {
@@ -137,6 +147,7 @@ server.registerTool("download_invoice", {
137
147
  format: z.enum(["pdf", "xml"]).default("pdf"),
138
148
  directory: z.string().optional().describe("Target folder, defaults to ~/Downloads"),
139
149
  },
150
+ outputSchema: output,
140
151
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
141
152
  }, ({ id, format, directory }) => respond(async () => {
142
153
  const file = await client.getFile(`/invoices/${id}.${format}`);
@@ -155,12 +166,24 @@ server.registerTool("record_payment", {
155
166
  payment_method: z.enum(["transfer", "cash"]).optional().describe("Defaults to transfer"),
156
167
  date: date.optional().describe("Defaults to today"),
157
168
  },
169
+ outputSchema: output,
158
170
  annotations: WRITE,
159
- }, (input) => respond(() => client.post("/payments", input)));
171
+ }, (input) => respond(async () => {
172
+ try {
173
+ return await client.post("/payments", input);
174
+ }
175
+ catch (error) {
176
+ if (error instanceof BillingEngineError && error.message.includes("Invoices is invalid")) {
177
+ throw new BillingEngineError(`${error.message} Payments can only be recorded for sent, open invoices of the same customer and tax rate; drafts cannot be paid.`);
178
+ }
179
+ throw error;
180
+ }
181
+ }));
160
182
  server.registerTool("list_payments", {
161
183
  title: "List payments",
162
184
  description: "Lists recorded payments, newest first, optionally only those for one invoice.",
163
185
  inputSchema: { invoice_id: z.number().int().optional(), page },
186
+ outputSchema: output,
164
187
  annotations: READ,
165
188
  }, (filters) => respond(() => client.get("/payments", filters)));
166
189
  await server.connect(new StdioServerTransport());
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "billingengine-mcp",
3
3
  "mcpName": "com.billingengine/mcp",
4
- "version": "0.1.0",
4
+ "version": "0.1.2",
5
5
  "description": "MCP server for BillingEngine: create customers and invoices, look up invoices and record payments from Claude, Cursor and other MCP clients.",
6
6
  "keywords": [
7
7
  "mcp",
@@ -13,7 +13,7 @@
13
13
  "xrechnung",
14
14
  "zugferd"
15
15
  ],
16
- "homepage": "https://www.billingengine.com/mcp",
16
+ "homepage": "https://www.billingengine.com/en/mcp",
17
17
  "repository": {
18
18
  "type": "git",
19
19
  "url": "git+https://github.com/billingengine/billingengine-mcp.git"
@@ -37,14 +37,18 @@
37
37
  },
38
38
  "scripts": {
39
39
  "build": "tsc && chmod +x dist/index.js",
40
- "prepublishOnly": "npm run build"
40
+ "prepublishOnly": "npm run build",
41
+ "bundle": "npm run build && esbuild dist/index.js --bundle --platform=node --format=esm --target=node20 --outfile=mcpb/server/index.js --banner:js=\"import { createRequire } from 'node:module'; const require = createRequire(import.meta.url);\" && mcpb pack mcpb billingengine.mcpb",
42
+ "bundle:smithery": "npm run bundle && node scripts/smithery-bundle.mjs"
41
43
  },
42
44
  "dependencies": {
43
45
  "@modelcontextprotocol/sdk": "^1.32.0",
44
46
  "zod": "^4.6.5"
45
47
  },
46
48
  "devDependencies": {
49
+ "@anthropic-ai/mcpb": "^2.1.2",
47
50
  "@types/node": "^26.6.4",
51
+ "esbuild": "^0.28.2",
48
52
  "typescript": "^7.0.2"
49
53
  }
50
54
  }