beeswax-mcp 1.3.0 → 1.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/README.md +30 -4
- package/dist/client.js +20 -0
- package/dist/index.js +14 -2
- package/dist/tools.js +381 -16
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,10 +6,21 @@ for creative businesses. It lets Claude and other MCP clients query your invoice
|
|
|
6
6
|
quotes, expenses, payments, journals, time entries, milestones, products & services
|
|
7
7
|
and tax returns.
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
else is strictly read-only.
|
|
9
|
+
Writes cover quotes, invoices and expenses (create, edit, finalise, void, version
|
|
10
|
+
restore), payments, manual journals, tax-return fields, the products & services
|
|
11
|
+
catalogue, freeform project documents and custom document themes — each gated
|
|
12
|
+
behind its own token scope. Everything else is strictly read-only.
|
|
13
|
+
|
|
14
|
+
**Ledger rule — read this first.** Every entry the server returns carries a
|
|
15
|
+
`posted` boolean. An entry is on the ledger **only when `posted` is true**.
|
|
16
|
+
Drafts, payroll templates, quotes and voided entries never affect the ledger,
|
|
17
|
+
whatever their `state`, `number` or line items say (a draft manual journal can
|
|
18
|
+
carry a number). Listings return posted entries by default; pass
|
|
19
|
+
`posted: false` for the unposted ones or `include_drafts: true` for both. A
|
|
20
|
+
`list_*_transactions` result for an unposted entry is `{ posted: false,
|
|
21
|
+
reason, transactions: [] }`; lines marked `preview: true` are not postings.
|
|
22
|
+
When summing by account or reconciling, use only results with `posted: true`.
|
|
23
|
+
See the changelog for the 1.4.0 behaviour change.
|
|
13
24
|
|
|
14
25
|
**Nothing here sends anything to your clients.** The quote tools create and edit
|
|
15
26
|
drafts; emailing a quote, and marking one accepted, stay human actions in the
|
|
@@ -113,12 +124,17 @@ reports the same on demand.
|
|
|
113
124
|
|---|---|
|
|
114
125
|
| `check_beeswax_connection` | `/meta` — connection health check: connector vs latest version, account, user, token scopes (no scope required) |
|
|
115
126
|
| `list_invoices`, `get_invoice`, `list_invoice_transactions` | `/invoices` |
|
|
127
|
+
| `create_invoice` (write), `update_invoice` (write), `finalise_invoice` (write), `void_invoice` (write), `delete_invoice` (write) | `/invoices` — create lands a draft; finalise posts; void retires a finalised unpaid one; delete is drafts only; no unfinalise |
|
|
128
|
+
| `list_invoice_groups`, `add_invoice_group` (write), `update_invoice_group` (write), `delete_invoice_group` (write), `add_invoice_line` (write), `update_invoice_line` (write), `delete_invoice_line` (write) | `/invoices/:id/transaction_groups[/:group_id/transactions]` |
|
|
129
|
+
| `list_invoice_versions`, `get_invoice_version`, `restore_invoice_version` (write) | `/invoices/:id/versions` — append-only history; restore lands as the next version |
|
|
116
130
|
| `list_quotes`, `get_quote`, `list_quote_transactions` | `/quotes` |
|
|
117
131
|
| `create_quote` (write), `update_quote` (write), `finalise_quote` (write), `delete_quote` (write) | `/quotes` |
|
|
118
132
|
| `list_quote_groups`, `add_quote_group` (write), `update_quote_group` (write), `delete_quote_group` (write), `reorder_quote_groups` (write) | `/quotes/:id/transaction_groups` |
|
|
119
133
|
| `add_quote_line` (write), `update_quote_line` (write), `delete_quote_line` (write), `reorder_quote_lines` (write) | `/quotes/:id/transaction_groups/:group_id/transactions` |
|
|
120
134
|
| `list_expenses`, `get_expense`, `list_expense_transactions` | `/expenses` |
|
|
135
|
+
| `create_expense`, `update_expense`, `finalise_expense`, `void_expense`, `delete_expense`, `list_expense_groups`, `add_expense_group`, `update_expense_group`, `delete_expense_group`, `add_expense_line`, `update_expense_line`, `delete_expense_line`, `list_expense_versions`, `get_expense_version`, `restore_expense_version` (writes as for invoices) | `/expenses` … |
|
|
121
136
|
| `query_payments`, `get_payment`, `list_payment_transactions` | `/payments` |
|
|
137
|
+
| `apply_payment` (write), `remove_payment` (write) | `/payments` — one payment per allocated document, one money side per call, all-or-nothing; no update |
|
|
122
138
|
| `query_journal_entries`, `get_journal_entry`, `list_journal_entry_transactions` | `/journal_entries` |
|
|
123
139
|
| `list_time_entries`, `get_time_entry` | `/time_entries` |
|
|
124
140
|
| `list_events`, `get_event` | `/events` (visibility-filtered) |
|
|
@@ -179,6 +195,16 @@ reports the same on demand.
|
|
|
179
195
|
a rotating default. On `update_quote_line` the link is all that changes: the
|
|
180
196
|
line keeps its numbers, so an already-priced quote can be re-pointed at the
|
|
181
197
|
catalogue without moving a total. Pass `null` to detach.
|
|
198
|
+
- **Invoices & expenses.** Same shape as quotes, different lifecycle: `create_*`
|
|
199
|
+
lands a DRAFT that is invisible to the ledger, tax reporting and Xero until
|
|
200
|
+
`finalise_*`; a finalised unpaid document is still editable in place; `void_*`
|
|
201
|
+
retires a finalised unpaid one (number, lines and history kept, Xero receives
|
|
202
|
+
VOIDED); `delete_*` is drafts only; paid means locked; there is no way back to
|
|
203
|
+
draft. Every write is refused inside the account lock date. Each document keeps
|
|
204
|
+
an append-only version history (`list_*_versions`, `restore_*_version`).
|
|
205
|
+
- **Payments.** `apply_payment` records one payment per allocated invoice/expense
|
|
206
|
+
from a bank account, one money side per call, all-or-nothing; `remove_payment`
|
|
207
|
+
reverts the document. Overpayment, currency and lock-date rules are the API's.
|
|
182
208
|
- **Quotes.** `create_quote` takes the whole document in one call — header,
|
|
183
209
|
sections and every line — because a real quote is several phases and dozens of
|
|
184
210
|
lines. Sections (`groups`) are the phases and keep the order you give them, as
|
package/dist/client.js
CHANGED
|
@@ -71,6 +71,19 @@ export class BeeswaxClient {
|
|
|
71
71
|
const body = await this.request(path, params);
|
|
72
72
|
return body?.[rootKey] ?? [];
|
|
73
73
|
}
|
|
74
|
+
// An entry's ledger postings WITH the envelope the API wraps them in:
|
|
75
|
+
// { transactions, posted, reason?, preview? }. `posted:false` means the
|
|
76
|
+
// entry has not hit the ledger and `transactions` is empty (unless the
|
|
77
|
+
// caller asked for a preview, in which case every line is marked preview).
|
|
78
|
+
async getPostings(path, params = {}) {
|
|
79
|
+
const body = await this.request(path, params);
|
|
80
|
+
return {
|
|
81
|
+
posted: body?.posted ?? null,
|
|
82
|
+
...(body?.reason !== undefined ? { reason: body.reason } : {}),
|
|
83
|
+
...(body?.preview !== undefined ? { preview: body.preview } : {}),
|
|
84
|
+
transactions: body?.transactions ?? [],
|
|
85
|
+
};
|
|
86
|
+
}
|
|
74
87
|
// Paginated list — walks every page (via meta.total_pages) and returns the
|
|
75
88
|
// merged array. Capped defensively so a malformed meta can't loop forever.
|
|
76
89
|
async fetchAll(path, rootKey, params = {}) {
|
|
@@ -120,6 +133,13 @@ export class BeeswaxClient {
|
|
|
120
133
|
const message = parsed && typeof parsed === "object" && parsed.error ? parsed.error : res.statusText;
|
|
121
134
|
throw new Error(`Beeswax API ${res.status} on ${method} ${path}: ${message}`);
|
|
122
135
|
}
|
|
136
|
+
// Every API destroy answers `head :no_content` (204, empty body). Returning
|
|
137
|
+
// `undefined` here made index.ts JSON.stringify to a non-string, which the
|
|
138
|
+
// MCP SDK rejected — so every successful delete surfaced as "Tool execution
|
|
139
|
+
// failed" after the record was already gone. Always hand back an object.
|
|
140
|
+
if (parsed === undefined) {
|
|
141
|
+
return { ok: true, status: res.status, method, path };
|
|
142
|
+
}
|
|
123
143
|
return parsed;
|
|
124
144
|
}
|
|
125
145
|
}
|
package/dist/index.js
CHANGED
|
@@ -20,7 +20,17 @@ if (!token) {
|
|
|
20
20
|
}
|
|
21
21
|
const client = new BeeswaxClient(baseUrl, token);
|
|
22
22
|
const toolsByName = new Map(TOOLS.map((tool) => [tool.name, tool]));
|
|
23
|
-
|
|
23
|
+
// Shown to the model as server instructions on connect. The ledger rule is
|
|
24
|
+
// here because the cost of getting it wrong is a wrong set of accounts: an
|
|
25
|
+
// earlier session summed a draft payroll template's lines as real wages.
|
|
26
|
+
const INSTRUCTIONS = "Beeswax is an accounting, project and time-tracking system. " +
|
|
27
|
+
"LEDGER RULE: an entry is on the ledger only when its `posted` field is true. " +
|
|
28
|
+
"Drafts, payroll templates, quotes and voided entries are never ledger activity, whatever their state, " +
|
|
29
|
+
"number or lines — never count them as transactions, wages, income or expenses. " +
|
|
30
|
+
"Listings return posted entries by default; pass posted:false or include_drafts:true only when the user " +
|
|
31
|
+
"wants drafts. A transactions result with posted:false has no postings; lines marked preview:true are " +
|
|
32
|
+
"not postings. When reconciling or summing by account, use only results with posted:true.";
|
|
33
|
+
const server = new Server({ name: "beeswax-mcp", version: VERSION }, { capabilities: { tools: {} }, instructions: INSTRUCTIONS });
|
|
24
34
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
25
35
|
tools: TOOLS.map(({ name, description, inputSchema }) => ({ name, description, inputSchema })),
|
|
26
36
|
}));
|
|
@@ -34,7 +44,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
34
44
|
}
|
|
35
45
|
try {
|
|
36
46
|
const result = await tool.handler(client, request.params.arguments ?? {});
|
|
37
|
-
|
|
47
|
+
// JSON.stringify(undefined) is undefined, not a string — a text-less
|
|
48
|
+
// content block fails the SDK's result schema and reads as a failed call.
|
|
49
|
+
let text = JSON.stringify(result ?? { ok: true }, null, 2) ?? "{}";
|
|
38
50
|
const update = client.updateAvailable;
|
|
39
51
|
if (update && !updateNoticeShown && request.params.name !== "check_beeswax_connection") {
|
|
40
52
|
updateNoticeShown = true;
|
package/dist/tools.js
CHANGED
|
@@ -59,16 +59,47 @@ const PAYABLE_LIST_PROPS = {
|
|
|
59
59
|
};
|
|
60
60
|
// invoices / quotes / expenses / payments all share list + show + transactions.
|
|
61
61
|
// `payable` adds the overdue/outstanding filters + due_on/overdue list fields.
|
|
62
|
+
// Every listing accepts these. The API returns only posted entries unless
|
|
63
|
+
// told otherwise; `posted:false` asks for the unposted ones, `include_drafts`
|
|
64
|
+
// for both. An explicit `state` filter switches the posted default off.
|
|
65
|
+
const POSTED_PROPS = {
|
|
66
|
+
posted: {
|
|
67
|
+
type: "boolean",
|
|
68
|
+
description: "Default true: only entries posted to the ledger. false: only unposted entries (drafts, templates, voided). " +
|
|
69
|
+
"Unposted entries never affect the ledger.",
|
|
70
|
+
},
|
|
71
|
+
include_drafts: { type: "boolean", description: "true: return posted and unposted entries together." },
|
|
72
|
+
};
|
|
73
|
+
const PREVIEW_PROP = {
|
|
74
|
+
preview: {
|
|
75
|
+
type: "boolean",
|
|
76
|
+
description: "For an UNPOSTED entry only: return the would-be postings, each marked preview:true. " +
|
|
77
|
+
"Never sum preview lines into balances.",
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
// One line per document type: which `state` values mean "on the ledger".
|
|
81
|
+
const LEDGER_STATES = {
|
|
82
|
+
invoices: "Ledger-effective states: finalised, pdf_sent, marked_sent, first_notice, second_notice, final_notice, partial_paid, paid. draft and voided are NOT on the ledger.",
|
|
83
|
+
expenses: "Ledger-effective states: finalised, pdf_sent, marked_sent, partial_paid, paid. draft and voided are NOT on the ledger.",
|
|
84
|
+
quotes: "Quotes are never on the ledger (posted is always false), whatever their state.",
|
|
85
|
+
payments: "Payments are posted on creation (state finalised/paid); a payment with posted:false is an anomaly, not activity.",
|
|
86
|
+
};
|
|
62
87
|
function accountingTools(resource, singular, label, listName, opts = {}) {
|
|
63
88
|
const payable = opts.payable ?? false;
|
|
64
89
|
const listProps = payable ? { ...ACCOUNTING_LIST_PROPS, ...PAYABLE_LIST_PROPS } : { ...ACCOUNTING_LIST_PROPS };
|
|
65
90
|
return [
|
|
66
91
|
{
|
|
67
92
|
name: listName,
|
|
68
|
-
description:
|
|
93
|
+
description: (resource === "quotes"
|
|
94
|
+
? `List all ${label} for the configured account. Quotes are never on the ledger (posted is always false). `
|
|
95
|
+
: `List ${label} posted to the ledger, unless posted:false or include_drafts:true. Drafts, templates and voided entries never affect the ledger. `) +
|
|
96
|
+
`Every entry carries \`posted\`; that flag — not state or number — says whether it is ledger activity. ` +
|
|
97
|
+
`Fetches every page and returns the merged array.` +
|
|
69
98
|
(payable ? " Pass overdue:true to get just the overdue ones (with due dates) in a single call." : ""),
|
|
70
|
-
inputSchema: { type: "object", properties: { ...listProps } },
|
|
99
|
+
inputSchema: { type: "object", properties: { ...listProps, ...POSTED_PROPS } },
|
|
71
100
|
handler: (client, args) => client.fetchAll(`/${resource}`, resource, {
|
|
101
|
+
posted: args.posted,
|
|
102
|
+
include_drafts: args.include_drafts,
|
|
72
103
|
state: args.state,
|
|
73
104
|
project_id: args.project_id,
|
|
74
105
|
company_id: args.company_id,
|
|
@@ -80,18 +111,336 @@ function accountingTools(resource, singular, label, listName, opts = {}) {
|
|
|
80
111
|
},
|
|
81
112
|
{
|
|
82
113
|
name: `get_${singular}`,
|
|
83
|
-
description: `Get a single ${singular} by id (full detail incl. totals and, where applicable, payment status)
|
|
114
|
+
description: `Get a single ${singular} by id (full detail incl. totals and, where applicable, payment status). ` +
|
|
115
|
+
`\`posted\` says whether it is on the ledger. ${LEDGER_STATES[resource] ?? ""}`,
|
|
84
116
|
inputSchema: { type: "object", properties: { id: { type: "integer" } }, required: ["id"] },
|
|
85
117
|
handler: (client, args) => client.getOne(`/${resource}/${args.id}`, singular),
|
|
86
118
|
},
|
|
87
119
|
{
|
|
88
120
|
name: `list_${singular}_transactions`,
|
|
89
|
-
description: `List the ledger postings (debit/credit lines) beneath a ${singular}
|
|
90
|
-
|
|
91
|
-
|
|
121
|
+
description: `List the ledger postings (debit/credit lines) beneath a ${singular}. ` +
|
|
122
|
+
`An unposted ${singular} (draft, template, voided) has NO postings: the result is posted:false with an empty ` +
|
|
123
|
+
`transactions array and a reason. Only sum lines from results with posted:true.`,
|
|
124
|
+
inputSchema: { type: "object", properties: { id: { type: "integer" }, ...SYSTEM_PROP, ...PREVIEW_PROP }, required: ["id"] },
|
|
125
|
+
handler: (client, args) => client.getPostings(`/${resource}/${args.id}/transactions`, { system: args.system, preview: args.preview }),
|
|
92
126
|
},
|
|
93
127
|
];
|
|
94
128
|
}
|
|
129
|
+
// ── Ledger documents: invoices and expenses (write) ─────────────────────
|
|
130
|
+
//
|
|
131
|
+
// Same shape as the quote tools, generated per document type. The lifecycle
|
|
132
|
+
// differs from quotes because these post to the ledger: create lands a
|
|
133
|
+
// DRAFT (invisible to the ledger, tax reporting and Xero until finalised),
|
|
134
|
+
// `finalise` posts it, `void` retires a finalised unpaid one (the number,
|
|
135
|
+
// lines and history are kept; Xero receives VOIDED), DELETE is drafts only,
|
|
136
|
+
// paid means locked, and there is NO way back to draft.
|
|
137
|
+
const LINE_INPUT_PROPS = {
|
|
138
|
+
description: { type: "string", description: "What the line is for." },
|
|
139
|
+
transaction_account_id: { type: "integer", description: "Chart-of-accounts id. Not needed if the product supplies one." },
|
|
140
|
+
transaction_template_id: {
|
|
141
|
+
type: "integer",
|
|
142
|
+
description: "Product/service to price this line from — see list_products_services. Anything you set yourself wins over the product's default.",
|
|
143
|
+
},
|
|
144
|
+
quantity: { type: "number", description: "Defaults to 1. Cannot be negative." },
|
|
145
|
+
unit_amount: { type: "number", description: "Per-unit price. Line total = unit_amount x quantity. Omit to use the product's rate." },
|
|
146
|
+
kind: { type: "string", description: "Unit label, e.g. 'Hours' or 'Units'." },
|
|
147
|
+
tax_id: { type: "integer", description: "Tax code — see list_taxes." },
|
|
148
|
+
};
|
|
149
|
+
function lineBody(args) {
|
|
150
|
+
return {
|
|
151
|
+
description: args.description,
|
|
152
|
+
transaction_account_id: args.transaction_account_id,
|
|
153
|
+
transaction_template_id: args.transaction_template_id,
|
|
154
|
+
quantity: args.quantity,
|
|
155
|
+
unit_amount: args.unit_amount,
|
|
156
|
+
kind: args.kind,
|
|
157
|
+
tax_id: args.tax_id,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
function ledgerDocumentTools(resource) {
|
|
161
|
+
const singular = resource === "invoices" ? "invoice" : "expense";
|
|
162
|
+
const Label = resource === "invoices" ? "invoice" : "expense (supplier bill)";
|
|
163
|
+
const scope = `${resource}:write`;
|
|
164
|
+
const readScope = `${resource}:read`;
|
|
165
|
+
const side = resource === "invoices" ? "sell" : "buy";
|
|
166
|
+
const partyProp = resource === "invoices"
|
|
167
|
+
? { company_id: { type: "integer", description: "Client the invoice is addressed to. Defaults to the project's client — see list_companies." } }
|
|
168
|
+
: { company_id: { type: "integer", description: "Supplier the bill is from. Defaults to the account's default contact — see list_companies." } };
|
|
169
|
+
const extraHeader = resource === "expenses" ? { external_number: { type: "string", description: "The supplier's own invoice number." } } : {};
|
|
170
|
+
const idKey = `${singular}_id`;
|
|
171
|
+
const lifecycle = `LIFECYCLE: creating lands a DRAFT — numbered, but NOT on the ledger, not in tax reporting and not sent to Xero until you call finalise_${singular}. ` +
|
|
172
|
+
`A finalised unpaid ${singular} can still be edited in place; once a payment is recorded it is locked. ` +
|
|
173
|
+
`There is no way back to draft: a finalised ${singular} that should not exist is voided (void_${singular}), never deleted. ` +
|
|
174
|
+
`Nothing here emails anyone.`;
|
|
175
|
+
return [
|
|
176
|
+
{
|
|
177
|
+
name: `create_${singular}`,
|
|
178
|
+
description: `Create a whole ${Label} in one call — header, sections and each section's lines. ${lifecycle} ` +
|
|
179
|
+
`Pass status:"finalised" to post it in the same call (refused, and nothing is created, if the total is negative, the project is locked, or the date falls on or before the account lock date). ` +
|
|
180
|
+
`Price lines from the catalogue with transaction_template_id where you can (${side}-side account, tax and rate are inherited). ` +
|
|
181
|
+
`project_id is required; company_id defaults as described. Every id must belong to your account — resolve them with list_projects, list_companies, list_transaction_accounts, list_taxes. ` +
|
|
182
|
+
`Requires ${scope}.`,
|
|
183
|
+
inputSchema: {
|
|
184
|
+
type: "object",
|
|
185
|
+
properties: {
|
|
186
|
+
project_id: { type: "integer", description: "Project the document belongs to. Required." },
|
|
187
|
+
...partyProp,
|
|
188
|
+
title: { type: "string" },
|
|
189
|
+
content: { type: "string", description: "Free-text notes." },
|
|
190
|
+
sent_on: { type: "string", description: "Document date, e.g. '2026-09-04'. Defaults to today." },
|
|
191
|
+
...extraHeader,
|
|
192
|
+
status: { type: "string", enum: ["draft", "finalised"], description: "Default 'draft'. 'finalised' posts it to the ledger in the same call." },
|
|
193
|
+
tax_inclusive: { type: "boolean" },
|
|
194
|
+
default_transaction_account_id: { type: "integer" },
|
|
195
|
+
default_tax_id: { type: "integer" },
|
|
196
|
+
groups: {
|
|
197
|
+
type: "array",
|
|
198
|
+
description: "Sections, in order.",
|
|
199
|
+
items: {
|
|
200
|
+
type: "object",
|
|
201
|
+
properties: {
|
|
202
|
+
name: { type: "string" },
|
|
203
|
+
due_on: { type: "string", description: "Optional section date." },
|
|
204
|
+
lines: {
|
|
205
|
+
type: "array",
|
|
206
|
+
items: {
|
|
207
|
+
type: "object",
|
|
208
|
+
properties: LINE_INPUT_PROPS,
|
|
209
|
+
anyOf: [{ required: ["transaction_account_id"] }, { required: ["transaction_template_id"] }],
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
required: ["name"],
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
required: ["project_id", "title"],
|
|
218
|
+
},
|
|
219
|
+
handler: (client, args) => client.mutate("POST", `/${resource}`, {
|
|
220
|
+
[singular]: {
|
|
221
|
+
project_id: args.project_id,
|
|
222
|
+
company_id: args.company_id,
|
|
223
|
+
title: args.title,
|
|
224
|
+
content: args.content,
|
|
225
|
+
sent_on: normalizeDate(args.sent_on),
|
|
226
|
+
external_number: args.external_number,
|
|
227
|
+
status: args.status,
|
|
228
|
+
tax_inclusive: args.tax_inclusive,
|
|
229
|
+
default_transaction_account_id: args.default_transaction_account_id,
|
|
230
|
+
default_tax_id: args.default_tax_id,
|
|
231
|
+
groups: (args.groups ?? []).map((g) => ({ ...g, due_on: normalizeDate(g.due_on) })),
|
|
232
|
+
},
|
|
233
|
+
}),
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
name: `update_${singular}`,
|
|
237
|
+
description: `Update a ${singular}'s HEADER — title, notes, date, ${resource === "invoices" ? "client" : "supplier, supplier reference"}, project, tax settings. Sections and lines are untouched; use the section and line tools for those. ` +
|
|
238
|
+
`Allowed while draft or finalised-unpaid; refused (422) once paid, part-paid, voided, or dated on or before the account lock date. Only the fields you pass change. Requires ${scope}.`,
|
|
239
|
+
inputSchema: {
|
|
240
|
+
type: "object",
|
|
241
|
+
properties: {
|
|
242
|
+
id: { type: "integer" },
|
|
243
|
+
title: { type: "string" },
|
|
244
|
+
content: { type: "string" },
|
|
245
|
+
sent_on: { type: "string" },
|
|
246
|
+
company_id: { type: "integer" },
|
|
247
|
+
project_id: { type: "integer" },
|
|
248
|
+
...extraHeader,
|
|
249
|
+
tax_inclusive: { type: "boolean" },
|
|
250
|
+
default_transaction_account_id: { type: "integer" },
|
|
251
|
+
default_tax_id: { type: "integer" },
|
|
252
|
+
},
|
|
253
|
+
required: ["id"],
|
|
254
|
+
},
|
|
255
|
+
handler: (client, args) => client.mutate("PATCH", `/${resource}/${args.id}`, {
|
|
256
|
+
[singular]: {
|
|
257
|
+
title: args.title,
|
|
258
|
+
content: args.content,
|
|
259
|
+
sent_on: normalizeDate(args.sent_on),
|
|
260
|
+
company_id: args.company_id,
|
|
261
|
+
project_id: args.project_id,
|
|
262
|
+
external_number: args.external_number,
|
|
263
|
+
tax_inclusive: args.tax_inclusive,
|
|
264
|
+
default_transaction_account_id: args.default_transaction_account_id,
|
|
265
|
+
default_tax_id: args.default_tax_id,
|
|
266
|
+
},
|
|
267
|
+
}),
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
name: `finalise_${singular}`,
|
|
271
|
+
description: `Post a DRAFT ${singular} to the ledger (draft → finalised). From then on it counts in reports and tax, and on Xero-connected accounts the next sync pushes it. ` +
|
|
272
|
+
`Refused if the total is negative, the project is locked, or the date is on or before the account lock date. There is no un-finalise. Does not email anyone. Requires ${scope}.`,
|
|
273
|
+
inputSchema: { type: "object", properties: { id: { type: "integer" } }, required: ["id"] },
|
|
274
|
+
handler: (client, args) => client.mutate("POST", `/${resource}/${args.id}/finalise`, {}),
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
name: `void_${singular}`,
|
|
278
|
+
description: `Void a finalised, UNPAID ${singular}: it keeps its number, lines and history and stays visible with state "voided", but leaves the ledger, tax reporting and ${resource === "invoices" ? "receivables" : "payables"}; Xero receives VOIDED on the next sync. Locked afterwards. ` +
|
|
279
|
+
`Refused for drafts (delete them instead), for a ${singular} with payments recorded (remove the payments first), for one already voided, and inside the account lock date. Requires ${scope}.`,
|
|
280
|
+
inputSchema: { type: "object", properties: { id: { type: "integer" } }, required: ["id"] },
|
|
281
|
+
handler: (client, args) => client.mutate("POST", `/${resource}/${args.id}/void`, {}),
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
name: `delete_${singular}`,
|
|
285
|
+
description: `Delete a DRAFT ${singular}. A finalised ${singular} is never deleted — void it instead (void_${singular}); the API says so with a 422. Requires ${scope}.`,
|
|
286
|
+
inputSchema: { type: "object", properties: { id: { type: "integer" } }, required: ["id"] },
|
|
287
|
+
handler: (client, args) => client.mutate("DELETE", `/${resource}/${args.id}`),
|
|
288
|
+
},
|
|
289
|
+
// Sections and lines.
|
|
290
|
+
{
|
|
291
|
+
name: `list_${singular}_groups`,
|
|
292
|
+
description: `List a ${singular}'s sections in order, each with its lines and total — the ids the section and line tools need. Requires ${readScope}.`,
|
|
293
|
+
inputSchema: { type: "object", properties: { [idKey]: { type: "integer" } }, required: [idKey] },
|
|
294
|
+
handler: (client, args) => client.getList(`/${resource}/${args[idKey]}/transaction_groups`, "transaction_groups"),
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
name: `add_${singular}_group`,
|
|
298
|
+
description: `Append a section to a ${singular}, optionally with its lines. Refused once the ${singular} is locked (paid, voided, lock date). Requires ${scope}.`,
|
|
299
|
+
inputSchema: {
|
|
300
|
+
type: "object",
|
|
301
|
+
properties: {
|
|
302
|
+
[idKey]: { type: "integer" },
|
|
303
|
+
name: { type: "string" },
|
|
304
|
+
due_on: { type: "string" },
|
|
305
|
+
lines: {
|
|
306
|
+
type: "array",
|
|
307
|
+
items: { type: "object", properties: LINE_INPUT_PROPS, anyOf: [{ required: ["transaction_account_id"] }, { required: ["transaction_template_id"] }] },
|
|
308
|
+
},
|
|
309
|
+
},
|
|
310
|
+
required: [idKey, "name"],
|
|
311
|
+
},
|
|
312
|
+
handler: (client, args) => client.mutate("POST", `/${resource}/${args[idKey]}/transaction_groups`, {
|
|
313
|
+
transaction_group: { name: args.name, due_on: normalizeDate(args.due_on), lines: args.lines },
|
|
314
|
+
}),
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
name: `update_${singular}_group`,
|
|
318
|
+
description: `Rename a ${singular} section or change its date. Lines are untouched. Requires ${scope}.`,
|
|
319
|
+
inputSchema: {
|
|
320
|
+
type: "object",
|
|
321
|
+
properties: { [idKey]: { type: "integer" }, group_id: { type: "integer" }, name: { type: "string" }, due_on: { type: "string" } },
|
|
322
|
+
required: [idKey, "group_id"],
|
|
323
|
+
},
|
|
324
|
+
handler: (client, args) => client.mutate("PATCH", `/${resource}/${args[idKey]}/transaction_groups/${args.group_id}`, {
|
|
325
|
+
transaction_group: { name: args.name, due_on: normalizeDate(args.due_on) },
|
|
326
|
+
}),
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
name: `delete_${singular}_group`,
|
|
330
|
+
description: `Delete a ${singular} section AND every line in it. Requires ${scope}.`,
|
|
331
|
+
inputSchema: { type: "object", properties: { [idKey]: { type: "integer" }, group_id: { type: "integer" } }, required: [idKey, "group_id"] },
|
|
332
|
+
handler: (client, args) => client.mutate("DELETE", `/${resource}/${args[idKey]}/transaction_groups/${args.group_id}`),
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
name: `add_${singular}_line`,
|
|
336
|
+
description: `Append one line to a ${singular} section. For a new document use create_${singular}. unit_amount is the per-unit price; Beeswax derives debit/credit from the account. ` +
|
|
337
|
+
`A transaction_template_id prices the line from that product (${side} side). Requires ${scope}.`,
|
|
338
|
+
inputSchema: {
|
|
339
|
+
type: "object",
|
|
340
|
+
properties: { [idKey]: { type: "integer" }, group_id: { type: "integer" }, ...LINE_INPUT_PROPS },
|
|
341
|
+
anyOf: [{ required: [idKey, "group_id", "transaction_account_id"] }, { required: [idKey, "group_id", "transaction_template_id"] }],
|
|
342
|
+
},
|
|
343
|
+
handler: (client, args) => client.mutate("POST", `/${resource}/${args[idKey]}/transaction_groups/${args.group_id}/transactions`, { transaction: lineBody(args) }),
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
name: `update_${singular}_line`,
|
|
347
|
+
description: `Change one line on a ${singular}. Only the fields you pass change; attaching a transaction_template_id links the product without repricing (null detaches). Requires ${scope}.`,
|
|
348
|
+
inputSchema: {
|
|
349
|
+
type: "object",
|
|
350
|
+
properties: {
|
|
351
|
+
[idKey]: { type: "integer" },
|
|
352
|
+
group_id: { type: "integer" },
|
|
353
|
+
line_id: { type: "integer" },
|
|
354
|
+
...LINE_INPUT_PROPS,
|
|
355
|
+
transaction_template_id: { type: ["integer", "null"], description: "Catalogue product; null detaches." },
|
|
356
|
+
},
|
|
357
|
+
required: [idKey, "group_id", "line_id"],
|
|
358
|
+
},
|
|
359
|
+
handler: (client, args) => client.mutate("PATCH", `/${resource}/${args[idKey]}/transaction_groups/${args.group_id}/transactions/${args.line_id}`, {
|
|
360
|
+
transaction: lineBody(args),
|
|
361
|
+
}),
|
|
362
|
+
},
|
|
363
|
+
{
|
|
364
|
+
name: `delete_${singular}_line`,
|
|
365
|
+
description: `Delete one line from a ${singular} section. Requires ${scope}.`,
|
|
366
|
+
inputSchema: {
|
|
367
|
+
type: "object",
|
|
368
|
+
properties: { [idKey]: { type: "integer" }, group_id: { type: "integer" }, line_id: { type: "integer" } },
|
|
369
|
+
required: [idKey, "group_id", "line_id"],
|
|
370
|
+
},
|
|
371
|
+
handler: (client, args) => client.mutate("DELETE", `/${resource}/${args[idKey]}/transaction_groups/${args.group_id}/transactions/${args.line_id}`),
|
|
372
|
+
},
|
|
373
|
+
// Version history.
|
|
374
|
+
{
|
|
375
|
+
name: `list_${singular}_versions`,
|
|
376
|
+
description: `List a ${singular}'s version history, newest first. A version is a whole-document snapshot (header, sections, lines) taken once per API write, once per web editing session, and on every state change; identical content is never stored twice. ` +
|
|
377
|
+
`Each entry says whether it is restorable (false once the ${singular} is paid, voided or inside the lock date). Requires ${readScope}.`,
|
|
378
|
+
inputSchema: { type: "object", properties: { [idKey]: { type: "integer" } }, required: [idKey] },
|
|
379
|
+
handler: (client, args) => client.getList(`/${resource}/${args[idKey]}/versions`, "versions"),
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
name: `get_${singular}_version`,
|
|
383
|
+
description: `Get one version of a ${singular} with its full payload — the header, sections and lines as they stood. Requires ${readScope}.`,
|
|
384
|
+
inputSchema: {
|
|
385
|
+
type: "object",
|
|
386
|
+
properties: { [idKey]: { type: "integer" }, version_number: { type: "integer" } },
|
|
387
|
+
required: [idKey, "version_number"],
|
|
388
|
+
},
|
|
389
|
+
handler: (client, args) => client.getOne(`/${resource}/${args[idKey]}/versions/${args.version_number}`, "version"),
|
|
390
|
+
},
|
|
391
|
+
{
|
|
392
|
+
name: `restore_${singular}_version`,
|
|
393
|
+
description: `Restore a ${singular} to an earlier version: header, sections and lines are rewritten to match it, and the result is recorded as the next version (history is never rewound). ` +
|
|
394
|
+
`Refused once the ${singular} is paid, part-paid, voided or inside the account lock date. Confirm with the user before restoring — it replaces the current lines. Requires ${scope}.`,
|
|
395
|
+
inputSchema: {
|
|
396
|
+
type: "object",
|
|
397
|
+
properties: { [idKey]: { type: "integer" }, version_number: { type: "integer" } },
|
|
398
|
+
required: [idKey, "version_number"],
|
|
399
|
+
},
|
|
400
|
+
handler: (client, args) => client.mutate("POST", `/${resource}/${args[idKey]}/versions/${args.version_number}/restore`, {}),
|
|
401
|
+
},
|
|
402
|
+
];
|
|
403
|
+
}
|
|
404
|
+
// ── Payments (write) ────────────────────────────────────────────────────
|
|
405
|
+
const PAYMENT_TOOLS = [
|
|
406
|
+
{
|
|
407
|
+
name: "apply_payment",
|
|
408
|
+
description: "Record a payment from a bank account against one or more FINALISED invoices (money in) or expenses (money out) — one side per call, never both. " +
|
|
409
|
+
"Each allocation becomes its own payment document and moves the settled invoice/expense to partial_paid or paid; the result lists the payments created and each document's new state and remaining balance. " +
|
|
410
|
+
"ALL-OR-NOTHING: if any allocation is refused, nothing is recorded. Refused when: the bank account is not this account's (see list_transaction_accounts, bank accounts only), the document is a draft or voided, the amount is not positive or exceeds what is still owing, the document is already fully paid, a document appears twice, the bank's currency differs from the document's, or the date is on or before the account lock date. " +
|
|
411
|
+
"There is no edit: remove a wrong payment (remove_payment) and apply it again. Confirm amounts with the user first. Requires payments:write.",
|
|
412
|
+
inputSchema: {
|
|
413
|
+
type: "object",
|
|
414
|
+
properties: {
|
|
415
|
+
bank_account_id: { type: "integer", description: "A transaction account flagged as a bank account." },
|
|
416
|
+
paid_on: { type: "string", description: "Payment date, e.g. '2026-09-04'." },
|
|
417
|
+
allocations: {
|
|
418
|
+
type: "array",
|
|
419
|
+
minItems: 1,
|
|
420
|
+
items: {
|
|
421
|
+
type: "object",
|
|
422
|
+
properties: {
|
|
423
|
+
journal_entry_id: { type: "integer", description: "A finalised invoice, expense or refund document." },
|
|
424
|
+
amount: { type: "number", description: "Positive; at most the amount still owing." },
|
|
425
|
+
},
|
|
426
|
+
required: ["journal_entry_id", "amount"],
|
|
427
|
+
},
|
|
428
|
+
},
|
|
429
|
+
},
|
|
430
|
+
required: ["bank_account_id", "paid_on", "allocations"],
|
|
431
|
+
},
|
|
432
|
+
handler: (client, args) => client.mutate("POST", "/payments", {
|
|
433
|
+
payment: { bank_account_id: args.bank_account_id, paid_on: normalizeDate(args.paid_on), allocations: args.allocations },
|
|
434
|
+
}),
|
|
435
|
+
},
|
|
436
|
+
{
|
|
437
|
+
name: "remove_payment",
|
|
438
|
+
description: "Remove a payment (by payment id — see query_payments or the result of apply_payment). The settled invoice/expense goes back to finalised or partial_paid and any bank-feed line allocated to it returns to pending. " +
|
|
439
|
+
"Refused for payroll payments and for payments dated on or before the account lock date. Confirm with the user first. Requires payments:write.",
|
|
440
|
+
inputSchema: { type: "object", properties: { id: { type: "integer", description: "Payment id." } }, required: ["id"] },
|
|
441
|
+
handler: (client, args) => client.mutate("DELETE", `/payments/${args.id}`),
|
|
442
|
+
},
|
|
443
|
+
];
|
|
95
444
|
export const TOOLS = [
|
|
96
445
|
{
|
|
97
446
|
name: "check_beeswax_connection",
|
|
@@ -122,31 +471,42 @@ export const TOOLS = [
|
|
|
122
471
|
...accountingTools("quotes", "quote", "quotes", "list_quotes"),
|
|
123
472
|
...accountingTools("expenses", "expense", "expense documents", "list_expenses", { payable: true }),
|
|
124
473
|
...accountingTools("payments", "payment", "payments (income / expense / payroll)", "query_payments"),
|
|
474
|
+
...ledgerDocumentTools("invoices"),
|
|
475
|
+
...ledgerDocumentTools("expenses"),
|
|
476
|
+
...PAYMENT_TOOLS,
|
|
125
477
|
// Generic journal-entries reader for the long tail.
|
|
126
478
|
{
|
|
127
479
|
name: "query_journal_entries",
|
|
128
|
-
description: "
|
|
480
|
+
description: "Returns only entries posted to the ledger unless posted:false (unposted only) or include_drafts:true (both). " +
|
|
481
|
+
"Drafts, payroll templates, quotes and voided entries never affect the ledger; every entry carries `posted`, " +
|
|
482
|
+
"and that flag — not state or number — is the ledger signal (a draft manual journal can carry a number). " +
|
|
483
|
+
"`type` accepts a URL segment (invoices) or class name (Invoice). " +
|
|
129
484
|
"Long-tail types (credits, bank_transfers, payrolls) and bare listings require an `all`-scoped token.",
|
|
130
485
|
inputSchema: {
|
|
131
486
|
type: "object",
|
|
132
487
|
properties: {
|
|
133
488
|
type: { type: "string", description: "STI type filter, e.g. invoices, payments, payrolls, credits, bank_transfers." },
|
|
134
|
-
state: { type: "string", description: "Filter by state." },
|
|
489
|
+
state: { type: "string", description: "Filter by state (switches the posted default off)." },
|
|
490
|
+
...POSTED_PROPS,
|
|
135
491
|
},
|
|
136
492
|
},
|
|
137
|
-
handler: (client, args) => client.fetchAll("/journal_entries", "journal_entries", {
|
|
493
|
+
handler: (client, args) => client.fetchAll("/journal_entries", "journal_entries", {
|
|
494
|
+
type: args.type, state: args.state, posted: args.posted, include_drafts: args.include_drafts,
|
|
495
|
+
}),
|
|
138
496
|
},
|
|
139
497
|
{
|
|
140
498
|
name: "get_journal_entry",
|
|
141
|
-
description: "Get a single journal entry by id (any type). Requires an `all`-scoped token.",
|
|
499
|
+
description: "Get a single journal entry by id (any type). `posted` says whether it is on the ledger. Requires an `all`-scoped token.",
|
|
142
500
|
inputSchema: { type: "object", properties: { id: { type: "integer" } }, required: ["id"] },
|
|
143
501
|
handler: (client, args) => client.getOne(`/journal_entries/${args.id}`, "journal_entry"),
|
|
144
502
|
},
|
|
145
503
|
{
|
|
146
504
|
name: "list_journal_entry_transactions",
|
|
147
|
-
description: "List the ledger postings beneath any journal entry by id."
|
|
148
|
-
|
|
149
|
-
|
|
505
|
+
description: "List the ledger postings beneath any journal entry by id. An unposted entry (draft, payroll template, quote, " +
|
|
506
|
+
"voided) has NO postings: the result is posted:false with an empty transactions array and a reason. " +
|
|
507
|
+
"Only sum lines from results with posted:true.",
|
|
508
|
+
inputSchema: { type: "object", properties: { id: { type: "integer" }, ...SYSTEM_PROP, ...PREVIEW_PROP }, required: ["id"] },
|
|
509
|
+
handler: (client, args) => client.getPostings(`/journal_entries/${args.id}/transactions`, { system: args.system, preview: args.preview }),
|
|
150
510
|
},
|
|
151
511
|
// Project / work cluster.
|
|
152
512
|
{
|
|
@@ -832,14 +1192,19 @@ export const TOOLS = [
|
|
|
832
1192
|
// ── Manual / general journal entries (RawJournalEntry) ──────────────────
|
|
833
1193
|
{
|
|
834
1194
|
name: "list_manual_journals",
|
|
835
|
-
description: "List manual (general) journal entries
|
|
1195
|
+
description: "List manual (general) journal entries posted to the ledger, newest first, unless posted:false (drafts only) or " +
|
|
1196
|
+
"include_drafts:true (both). A draft may already carry a number — `posted` is the ledger signal, `number` is not. " +
|
|
1197
|
+
"Requires journal_entries:read (write implies read).",
|
|
836
1198
|
inputSchema: {
|
|
837
1199
|
type: "object",
|
|
838
1200
|
properties: {
|
|
839
|
-
state: { type: "string", enum: ["draft", "finalised"], description: "Optional state filter." },
|
|
1201
|
+
state: { type: "string", enum: ["draft", "finalised"], description: "Optional state filter (switches the posted default off)." },
|
|
1202
|
+
...POSTED_PROPS,
|
|
840
1203
|
},
|
|
841
1204
|
},
|
|
842
|
-
handler: (client, args) => client.fetchAll("/raw_journal_entries", "raw_journal_entries", {
|
|
1205
|
+
handler: (client, args) => client.fetchAll("/raw_journal_entries", "raw_journal_entries", {
|
|
1206
|
+
state: args.state, posted: args.posted, include_drafts: args.include_drafts,
|
|
1207
|
+
}),
|
|
843
1208
|
},
|
|
844
1209
|
{
|
|
845
1210
|
name: "get_manual_journal",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "beeswax-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "Official MCP server for Beeswax (beeswaxapp.com) — query invoices, quotes, expenses, payments, journals, time entries, projects, products & services and tax returns from Claude and other MCP clients, and build quotes priced from your catalogue.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://www.beeswaxapp.com/support/mcp-setup",
|