billingengine-mcp 0.1.0 → 0.1.1

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
@@ -4,16 +4,16 @@ Create customers and invoices, look up open invoices and record payments in
4
4
  [BillingEngine](https://www.billingengine.com) by talking to Claude, Cursor or any other
5
5
  [MCP](https://modelcontextprotocol.io) client:
6
6
 
7
- > "Create an invoice for Malermeister Müller over 100 euros."
7
+ > "Create an invoice for Acme Painting over 100 euros."
8
8
  >
9
9
  > "Which invoices are overdue?"
10
10
  >
11
- > "Müller paid invoice 2610030028, record the payment."
11
+ > "Acme paid invoice 2610030028, record the payment."
12
12
 
13
13
  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.
14
+ [e-invoicing](https://www.billingengine.com/en/features) (EN 16931, XRechnung, ZUGFeRD / Factur-X).
15
+ The server is a thin layer over the [BillingEngine API](https://www.billingengine.com/en/api)
16
+ and runs on your computer.
17
17
 
18
18
  ## Requirements
19
19
 
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.1" }, { 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({
@@ -58,10 +58,12 @@ const invoiceFields = {
58
58
  };
59
59
  const READ = { readOnlyHint: true, openWorldHint: false };
60
60
  const WRITE = { readOnlyHint: false, destructiveHint: false, openWorldHint: false };
61
+ const output = z.looseObject({});
61
62
  async function respond(action) {
62
63
  try {
63
64
  const value = await action();
64
- return { content: [{ type: "text", text: typeof value === "string" ? value : JSON.stringify(value, null, 2) }] };
65
+ const structured = typeof value === "string" ? { message: value } : value;
66
+ return { content: [{ type: "text", text: JSON.stringify(structured, null, 2) }], structuredContent: structured };
65
67
  }
66
68
  catch (error) {
67
69
  const message = error instanceof BillingEngineError ? error.message : `Unexpected error: ${error.message}`;
@@ -81,18 +83,21 @@ server.registerTool("find_customers", {
81
83
  title: "Find customers",
82
84
  description: "Lists customers, optionally filtered by a search text that matches company, names and email.",
83
85
  inputSchema: { query: z.string().optional(), page },
86
+ outputSchema: output,
84
87
  annotations: READ,
85
88
  }, ({ query, page }) => respond(() => client.get("/customers", { query, page })));
86
89
  server.registerTool("create_customer", {
87
90
  title: "Create a customer",
88
91
  description: "Creates a customer. Needs a company or a last name and a full address (line 1, postal code, city, country).",
89
92
  inputSchema: { ...customerFields, address },
93
+ outputSchema: output,
90
94
  annotations: WRITE,
91
95
  }, ({ address, ...fields }) => respond(() => client.post("/customers", { ...fields, address_attributes: address })));
92
96
  server.registerTool("update_customer", {
93
97
  title: "Update a customer",
94
98
  description: "Changes the given fields of a customer; fields that are left out stay as they are.",
95
99
  inputSchema: { id: z.number().int(), ...customerFields, address: address.partial().optional() },
100
+ outputSchema: output,
96
101
  annotations: { ...WRITE, idempotentHint: true },
97
102
  }, ({ id, address, ...fields }) => respond(() => client.patch(`/customers/${id}`, { ...fields, ...(address && { address_attributes: address }) })));
98
103
  server.registerTool("create_invoice", {
@@ -100,12 +105,14 @@ server.registerTool("create_invoice", {
100
105
  description: "Creates a draft invoice for a customer. Number, language, texts and VAT treatment come from the account settings. " +
101
106
  "The invoice is not sent; it counts towards the monthly invoice maximum of the plan.",
102
107
  inputSchema: { customer_id: z.number().int(), items: z.array(item).min(1), ...invoiceFields },
108
+ outputSchema: output,
103
109
  annotations: WRITE,
104
110
  }, (input) => respond(async () => withInvoiceLinks(await client.post("/invoices", input))));
105
111
  server.registerTool("update_invoice", {
106
112
  title: "Update a draft invoice",
107
113
  description: "Changes a draft invoice. Passing items replaces all existing items, so send the complete list. Sent invoices cannot be changed.",
108
114
  inputSchema: { id: z.number().int(), items: z.array(item).min(1).optional(), ...invoiceFields },
115
+ outputSchema: output,
109
116
  annotations: { ...WRITE, idempotentHint: true },
110
117
  }, ({ id, ...changes }) => respond(async () => withInvoiceLinks(await client.patch(`/invoices/${id}`, changes))));
111
118
  server.registerTool("list_invoices", {
@@ -120,12 +127,14 @@ server.registerTool("list_invoices", {
120
127
  page,
121
128
  per_page: z.number().int().min(1).max(100).optional(),
122
129
  },
130
+ outputSchema: output,
123
131
  annotations: READ,
124
132
  }, (filters) => respond(async () => withInvoiceLinks(await client.get("/invoices", filters))));
125
133
  server.registerTool("get_invoice", {
126
134
  title: "Get an invoice",
127
135
  description: "Returns one invoice with its items, amounts and payment status.",
128
136
  inputSchema: { id: z.number().int() },
137
+ outputSchema: output,
129
138
  annotations: READ,
130
139
  }, ({ id }) => respond(async () => withInvoiceLinks(await client.get(`/invoices/${id}`))));
131
140
  server.registerTool("download_invoice", {
@@ -137,6 +146,7 @@ server.registerTool("download_invoice", {
137
146
  format: z.enum(["pdf", "xml"]).default("pdf"),
138
147
  directory: z.string().optional().describe("Target folder, defaults to ~/Downloads"),
139
148
  },
149
+ outputSchema: output,
140
150
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
141
151
  }, ({ id, format, directory }) => respond(async () => {
142
152
  const file = await client.getFile(`/invoices/${id}.${format}`);
@@ -155,12 +165,24 @@ server.registerTool("record_payment", {
155
165
  payment_method: z.enum(["transfer", "cash"]).optional().describe("Defaults to transfer"),
156
166
  date: date.optional().describe("Defaults to today"),
157
167
  },
168
+ outputSchema: output,
158
169
  annotations: WRITE,
159
- }, (input) => respond(() => client.post("/payments", input)));
170
+ }, (input) => respond(async () => {
171
+ try {
172
+ return await client.post("/payments", input);
173
+ }
174
+ catch (error) {
175
+ if (error instanceof BillingEngineError && error.message.includes("Invoices is invalid")) {
176
+ 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.`);
177
+ }
178
+ throw error;
179
+ }
180
+ }));
160
181
  server.registerTool("list_payments", {
161
182
  title: "List payments",
162
183
  description: "Lists recorded payments, newest first, optionally only those for one invoice.",
163
184
  inputSchema: { invoice_id: z.number().int().optional(), page },
185
+ outputSchema: output,
164
186
  annotations: READ,
165
187
  }, (filters) => respond(() => client.get("/payments", filters)));
166
188
  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.1",
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
  }