beeswax-mcp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ryanchow Pty Ltd (beeswaxapp.com)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,123 @@
1
+ # beeswax-mcp
2
+
3
+ The official [Model Context Protocol](https://modelcontextprotocol.io) server for
4
+ [Beeswax](https://www.beeswaxapp.com) — projects, tasks, time tracking and accounting
5
+ for creative businesses. It lets Claude and other MCP clients query your invoices,
6
+ quotes, expenses, payments, journals, time entries, milestones and tax returns.
7
+
8
+ It is **read-mostly**: writes are limited to manual journals and tax-return fields,
9
+ and each write is gated behind its own token scope — everything else is strictly
10
+ read-only.
11
+
12
+ ## Quick start
13
+
14
+ You need **Node.js ≥ 18** and a Beeswax API token (see below).
15
+
16
+ ### Claude Desktop
17
+
18
+ Add to `claude_desktop_config.json` (Settings → Developer → Edit Config):
19
+
20
+ ```json
21
+ {
22
+ "mcpServers": {
23
+ "beeswax": {
24
+ "command": "npx",
25
+ "args": ["-y", "beeswax-mcp"],
26
+ "env": {
27
+ "BEESWAX_API_TOKEN": "your-token"
28
+ }
29
+ }
30
+ }
31
+ }
32
+ ```
33
+
34
+ ### Claude Code
35
+
36
+ ```bash
37
+ claude mcp add beeswax --env BEESWAX_API_TOKEN=your-token -- npx -y beeswax-mcp
38
+ ```
39
+
40
+ ### Any other MCP client
41
+
42
+ Run `npx -y beeswax-mcp` as a stdio server with `BEESWAX_API_TOKEN` set in the
43
+ environment.
44
+
45
+ ## Getting an API token
46
+
47
+ Create a token in Beeswax under **Account Settings → API Tokens** (Owner or
48
+ Super Admin only; requires the accounting add-on). The token is bound to one
49
+ account.
50
+
51
+ **Grant only the scopes you need.** Tick individual read scopes
52
+ (`invoices:read`, `payments:read`, `transactions:read`, `journal_entries:read`,
53
+ `tax_returns:read`, `events:read`, `milestones:read`, `projects:read`,
54
+ `time_entries:read`, …) rather than `all` — `all` grants read **and** write. Add
55
+ `journal_entries:write` / `tax_returns:write` only if the agent should be able to
56
+ use the write tools.
57
+
58
+ **Keep the token secret.** It sits in plain text in your client's config file, and
59
+ anyone who can read that file can read your account data. Revoke tokens you no
60
+ longer use from the same settings page.
61
+
62
+ ## Configuration
63
+
64
+ | Environment variable | Required | Default |
65
+ |---|---|---|
66
+ | `BEESWAX_API_TOKEN` | yes | — |
67
+ | `BEESWAX_BASE_URL` | no | `https://app.beeswaxapp.com/new_api/v1` |
68
+
69
+ ## Tools
70
+
71
+ | Tool | Endpoint |
72
+ |---|---|
73
+ | `list_invoices`, `get_invoice`, `list_invoice_transactions` | `/invoices` |
74
+ | `list_quotes`, `get_quote`, `list_quote_transactions` | `/quotes` |
75
+ | `list_expenses`, `get_expense`, `list_expense_transactions` | `/expenses` |
76
+ | `query_payments`, `get_payment`, `list_payment_transactions` | `/payments` |
77
+ | `query_journal_entries`, `get_journal_entry`, `list_journal_entry_transactions` | `/journal_entries` |
78
+ | `list_time_entries`, `get_time_entry` | `/time_entries` |
79
+ | `list_events`, `get_event` | `/events` (visibility-filtered) |
80
+ | `list_milestones`, `get_milestone` | `/milestones` |
81
+ | `get_project` | `/active_account/projects/:id` |
82
+ | `list_manual_journals`, `get_manual_journal`, `create_manual_journal` (write), `finalise_manual_journal` (write) | `/raw_journal_entries` |
83
+ | `list_tax_returns`, `get_tax_return`, `get_tax_return_field_transactions`, `list_tax_return_findings`, `update_tax_return_field` (write), `confirm_tax_return_section` (write) | `/tax_returns` |
84
+
85
+ ### Notes
86
+
87
+ - **Scopes.** `query_journal_entries` / `get_journal_entry` need an `all`-scoped
88
+ token for a bare listing, a show, or a long-tail type (credits, bank_transfers,
89
+ payrolls); a typed listing (`type: "invoices"`) accepts that type's read scope.
90
+ - **Transactions filter.** `system: "true"` returns only system postings (Trade
91
+ Debtors / Tax); omitted / `"false"` returns only the non-system line items.
92
+ - **Overdue / outstanding (invoices & expenses only).** `overdue: true` returns
93
+ just the entries past their due date and not fully paid — those rows also carry
94
+ `due_on` and `overdue`, so "what's overdue?" is one call, not a list + N detail
95
+ fetches. `outstanding: true` is the looser "not paid/draft, any due date" set.
96
+ - **Dates.** `from` / `to` accept ISO (`2026-01-01`) or loose strings
97
+ (`Jan 1 2026`); they are normalized to `YYYY-MM-DD` before being sent.
98
+ - **Manual journals.** Balanced double-entry enforced by the API (≥ 2 lines, one of
99
+ debit/credit per line, debits = credits). Created finalised by default; pass
100
+ `status: "draft"` and use `finalise_manual_journal` for a review step. Needs
101
+ `journal_entries:read` / `journal_entries:write`.
102
+ - **Tax returns.** Australian accounts only (feature-gated server-side). Reads need
103
+ `tax_returns:read`; `update_tax_return_field` / `confirm_tax_return_section` need
104
+ `tax_returns:write` and refuse calculated fields and finalised/lodged returns.
105
+
106
+ ## Development
107
+
108
+ Building from source (in the Beeswax repo, `mcp/` folder):
109
+
110
+ ```bash
111
+ cd mcp
112
+ npm install
113
+ npm run build # tsc → dist/
114
+ ```
115
+
116
+ The repo's default `node` may be older than 18 — use nvm/Homebrew (e.g. `nvm use 20`).
117
+ Point your MCP client at `node /absolute/path/to/mcp/dist/index.js` instead of `npx`
118
+ to run a local build. The underlying HTTP API is documented in `docs/openapi.yaml`
119
+ in the main repo.
120
+
121
+ ## Support
122
+
123
+ Setup guide: <https://www.beeswaxapp.com/support/mcp-setup>
package/dist/client.js ADDED
@@ -0,0 +1,116 @@
1
+ // Thin HTTP client for the Beeswax new_api/v1 REST API.
2
+ //
3
+ // Responsibilities the MCP tools delegate here:
4
+ // - Bearer-token auth (from BEESWAX_API_TOKEN)
5
+ // - collapsing pagination into "fetch every page"
6
+ // - turning non-2xx responses into clear Error messages
7
+ export class BeeswaxClient {
8
+ token;
9
+ baseUrl;
10
+ constructor(baseUrl, token) {
11
+ this.token = token;
12
+ this.baseUrl = baseUrl.replace(/\/+$/, "");
13
+ }
14
+ async request(path, params = {}) {
15
+ const url = new URL(this.baseUrl + path);
16
+ for (const [key, value] of Object.entries(params)) {
17
+ if (value !== undefined && value !== null && value !== "") {
18
+ url.searchParams.set(key, String(value));
19
+ }
20
+ }
21
+ let res;
22
+ try {
23
+ res = await fetch(url, {
24
+ headers: { Authorization: `Bearer ${this.token}`, Accept: "application/json" },
25
+ });
26
+ }
27
+ catch (err) {
28
+ throw new Error(`Could not reach Beeswax at ${url.origin}: ${err.message}`);
29
+ }
30
+ const text = await res.text();
31
+ let body;
32
+ try {
33
+ body = text ? JSON.parse(text) : undefined;
34
+ }
35
+ catch {
36
+ body = text;
37
+ }
38
+ if (!res.ok) {
39
+ const message = body && typeof body === "object" && body.error ? body.error : res.statusText;
40
+ throw new Error(`Beeswax API ${res.status} on GET ${path}: ${message}`);
41
+ }
42
+ return body;
43
+ }
44
+ // Single resource — returns the value nested under `rootKey` (e.g. "invoice").
45
+ async getOne(path, rootKey) {
46
+ const body = await this.request(path);
47
+ return rootKey ? body?.[rootKey] : body;
48
+ }
49
+ // Non-paginated array (e.g. an entry's transactions).
50
+ async getList(path, rootKey, params = {}) {
51
+ const body = await this.request(path, params);
52
+ return body?.[rootKey] ?? [];
53
+ }
54
+ // Paginated list — walks every page (via meta.total_pages) and returns the
55
+ // merged array. Capped defensively so a malformed meta can't loop forever.
56
+ async fetchAll(path, rootKey, params = {}) {
57
+ const perPage = 100;
58
+ const all = [];
59
+ let page = 1;
60
+ let totalPages = 1;
61
+ do {
62
+ const body = await this.request(path, { ...params, page, per_page: perPage });
63
+ const items = body?.[rootKey] ?? [];
64
+ all.push(...items);
65
+ totalPages = Number(body?.meta?.total_pages ?? 1) || 1;
66
+ page += 1;
67
+ } while (page <= totalPages && all.length < 20000);
68
+ return all;
69
+ }
70
+ // Write request (POST / PATCH / DELETE) with a JSON body. Returns the parsed
71
+ // response body, or throws with the API's error message on a non-2xx.
72
+ async mutate(method, path, body = {}) {
73
+ const url = new URL(this.baseUrl + path);
74
+ let res;
75
+ try {
76
+ res = await fetch(url, {
77
+ method,
78
+ headers: {
79
+ Authorization: `Bearer ${this.token}`,
80
+ Accept: "application/json",
81
+ "Content-Type": "application/json",
82
+ },
83
+ body: JSON.stringify(body),
84
+ });
85
+ }
86
+ catch (err) {
87
+ throw new Error(`Could not reach Beeswax at ${url.origin}: ${err.message}`);
88
+ }
89
+ const text = await res.text();
90
+ let parsed;
91
+ try {
92
+ parsed = text ? JSON.parse(text) : undefined;
93
+ }
94
+ catch {
95
+ parsed = text;
96
+ }
97
+ if (!res.ok) {
98
+ const message = parsed && typeof parsed === "object" && parsed.error ? parsed.error : res.statusText;
99
+ throw new Error(`Beeswax API ${res.status} on ${method} ${path}: ${message}`);
100
+ }
101
+ return parsed;
102
+ }
103
+ }
104
+ // Normalize a natural-ish date string to YYYY-MM-DD (what the API filters expect).
105
+ // Already-ISO and unparseable strings pass through untouched.
106
+ export function normalizeDate(input) {
107
+ if (!input)
108
+ return undefined;
109
+ const s = String(input).trim();
110
+ if (/^\d{4}-\d{2}-\d{2}$/.test(s))
111
+ return s;
112
+ const parsed = new Date(s);
113
+ if (Number.isNaN(parsed.getTime()))
114
+ return s;
115
+ return parsed.toISOString().slice(0, 10);
116
+ }
package/dist/index.js ADDED
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env node
2
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
5
+ import { BeeswaxClient } from "./client.js";
6
+ import { TOOLS } from "./tools.js";
7
+ const token = process.env.BEESWAX_API_TOKEN;
8
+ const baseUrl = process.env.BEESWAX_BASE_URL ?? "https://app.beeswaxapp.com/new_api/v1";
9
+ if (!token) {
10
+ console.error("BEESWAX_API_TOKEN is required. Create an API token in Beeswax " +
11
+ "(Account Settings → API Tokens) with the scopes you need — add tax_returns:write " +
12
+ "to edit tax-return figures — and set it in the environment.");
13
+ process.exit(1);
14
+ }
15
+ const client = new BeeswaxClient(baseUrl, token);
16
+ const toolsByName = new Map(TOOLS.map((tool) => [tool.name, tool]));
17
+ const server = new Server({ name: "beeswax-mcp", version: "0.1.0" }, { capabilities: { tools: {} } });
18
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
19
+ tools: TOOLS.map(({ name, description, inputSchema }) => ({ name, description, inputSchema })),
20
+ }));
21
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
22
+ const tool = toolsByName.get(request.params.name);
23
+ if (!tool) {
24
+ return { content: [{ type: "text", text: `Unknown tool: ${request.params.name}` }], isError: true };
25
+ }
26
+ try {
27
+ const result = await tool.handler(client, request.params.arguments ?? {});
28
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
29
+ }
30
+ catch (err) {
31
+ return { content: [{ type: "text", text: `Error: ${err.message}` }], isError: true };
32
+ }
33
+ });
34
+ async function main() {
35
+ await server.connect(new StdioServerTransport());
36
+ // stderr only — stdout is the MCP protocol channel.
37
+ console.error(`beeswax-mcp ready → ${baseUrl}; ${TOOLS.length} tools (read + tax-return write)`);
38
+ }
39
+ main().catch((err) => {
40
+ console.error("Fatal:", err);
41
+ process.exit(1);
42
+ });
package/dist/tools.js ADDED
@@ -0,0 +1,308 @@
1
+ import { normalizeDate } from "./client.js";
2
+ const SYSTEM_PROP = {
3
+ system: {
4
+ type: "string",
5
+ enum: ["true", "false"],
6
+ description: "true = only system postings (Trade Debtors/Tax); false or omitted = only non-system line items.",
7
+ },
8
+ };
9
+ const DATE_PROPS = {
10
+ from: {
11
+ type: "string",
12
+ description: "Earliest document date (sent_on). Accepts natural strings, e.g. '2026-01-01' or 'Jan 1 2026'.",
13
+ },
14
+ to: { type: "string", description: "Latest document date (sent_on). Same format as `from`." },
15
+ };
16
+ const ACCOUNTING_LIST_PROPS = {
17
+ state: { type: "string", description: "Filter by state (draft, finalised, pdf_sent, partial_paid, paid, …)." },
18
+ project_id: { type: "integer", description: "Filter to a single project." },
19
+ ...DATE_PROPS,
20
+ };
21
+ // Only the payable types (invoices / expenses) carry a due date, so the
22
+ // overdue / outstanding filters are exposed for those tools only.
23
+ const PAYABLE_LIST_PROPS = {
24
+ overdue: {
25
+ type: "boolean",
26
+ description: "true = only entries past their due date and not fully paid (state ≠ paid/draft, sent_on older than the account payment term). The list rows then include `due_on` and `overdue`.",
27
+ },
28
+ outstanding: {
29
+ type: "boolean",
30
+ description: "true = only entries not fully settled (any state except paid/draft), regardless of due date.",
31
+ },
32
+ };
33
+ // invoices / quotes / expenses / payments all share list + show + transactions.
34
+ // `payable` adds the overdue/outstanding filters + due_on/overdue list fields.
35
+ function accountingTools(resource, singular, label, listName, opts = {}) {
36
+ const payable = opts.payable ?? false;
37
+ const listProps = payable ? { ...ACCOUNTING_LIST_PROPS, ...PAYABLE_LIST_PROPS } : { ...ACCOUNTING_LIST_PROPS };
38
+ return [
39
+ {
40
+ name: listName,
41
+ description: `List all ${label} for the configured account. Fetches every page and returns the merged array.` +
42
+ (payable ? " Pass overdue:true to get just the overdue ones (with due dates) in a single call." : ""),
43
+ inputSchema: { type: "object", properties: { ...listProps } },
44
+ handler: (client, args) => client.fetchAll(`/${resource}`, resource, {
45
+ state: args.state,
46
+ project_id: args.project_id,
47
+ from: normalizeDate(args.from),
48
+ to: normalizeDate(args.to),
49
+ ...(payable ? { overdue: args.overdue, outstanding: args.outstanding } : {}),
50
+ }),
51
+ },
52
+ {
53
+ name: `get_${singular}`,
54
+ description: `Get a single ${singular} by id (full detail incl. totals and, where applicable, payment status).`,
55
+ inputSchema: { type: "object", properties: { id: { type: "integer" } }, required: ["id"] },
56
+ handler: (client, args) => client.getOne(`/${resource}/${args.id}`, singular),
57
+ },
58
+ {
59
+ name: `list_${singular}_transactions`,
60
+ description: `List the ledger postings (debit/credit lines) beneath a ${singular}.`,
61
+ inputSchema: { type: "object", properties: { id: { type: "integer" }, ...SYSTEM_PROP }, required: ["id"] },
62
+ handler: (client, args) => client.getList(`/${resource}/${args.id}/transactions`, "transactions", { system: args.system }),
63
+ },
64
+ ];
65
+ }
66
+ export const TOOLS = [
67
+ ...accountingTools("invoices", "invoice", "invoices", "list_invoices", { payable: true }),
68
+ ...accountingTools("quotes", "quote", "quotes", "list_quotes"),
69
+ ...accountingTools("expenses", "expense", "expense documents", "list_expenses", { payable: true }),
70
+ ...accountingTools("payments", "payment", "payments (income / expense / payroll)", "query_payments"),
71
+ // Generic journal-entries reader for the long tail.
72
+ {
73
+ name: "query_journal_entries",
74
+ description: "List journal entries of any type. `type` accepts a URL segment (invoices) or class name (Invoice). " +
75
+ "Long-tail types (credits, bank_transfers, payrolls) and bare listings require an `all`-scoped token.",
76
+ inputSchema: {
77
+ type: "object",
78
+ properties: {
79
+ type: { type: "string", description: "STI type filter, e.g. invoices, payments, payrolls, credits, bank_transfers." },
80
+ state: { type: "string", description: "Filter by state." },
81
+ },
82
+ },
83
+ handler: (client, args) => client.fetchAll("/journal_entries", "journal_entries", { type: args.type, state: args.state }),
84
+ },
85
+ {
86
+ name: "get_journal_entry",
87
+ description: "Get a single journal entry by id (any type). Requires an `all`-scoped token.",
88
+ inputSchema: { type: "object", properties: { id: { type: "integer" } }, required: ["id"] },
89
+ handler: (client, args) => client.getOne(`/journal_entries/${args.id}`, "journal_entry"),
90
+ },
91
+ {
92
+ name: "list_journal_entry_transactions",
93
+ description: "List the ledger postings beneath any journal entry by id.",
94
+ inputSchema: { type: "object", properties: { id: { type: "integer" }, ...SYSTEM_PROP }, required: ["id"] },
95
+ handler: (client, args) => client.getList(`/journal_entries/${args.id}/transactions`, "transactions", { system: args.system }),
96
+ },
97
+ // Project / work cluster.
98
+ {
99
+ name: "list_time_entries",
100
+ description: "List all time entries for the configured account (every page).",
101
+ inputSchema: {
102
+ type: "object",
103
+ properties: {
104
+ project_id: { type: "integer", description: "Filter to a single project." },
105
+ billable: { type: "boolean", description: "Filter by billable flag." },
106
+ },
107
+ },
108
+ handler: (client, args) => client.fetchAll("/time_entries", "time_entries", { project_id: args.project_id, billable: args.billable }),
109
+ },
110
+ {
111
+ name: "get_time_entry",
112
+ description: "Get a single time entry by id.",
113
+ inputSchema: { type: "object", properties: { id: { type: "integer" } }, required: ["id"] },
114
+ handler: (client, args) => client.getOne(`/time_entries/${args.id}`, "time_entry"),
115
+ },
116
+ {
117
+ name: "list_events",
118
+ description: "List calendar events visible to the token's user (private events the user is not invited to are excluded).",
119
+ inputSchema: {
120
+ type: "object",
121
+ properties: { project_id: { type: "integer", description: "Filter to a single project." }, ...DATE_PROPS },
122
+ },
123
+ handler: (client, args) => client.fetchAll("/events", "events", {
124
+ project_id: args.project_id,
125
+ from: normalizeDate(args.from),
126
+ to: normalizeDate(args.to),
127
+ }),
128
+ },
129
+ {
130
+ name: "get_event",
131
+ description: "Get a single event by id (returns not-found if it is not visible to the token's user).",
132
+ inputSchema: { type: "object", properties: { id: { type: "integer" } }, required: ["id"] },
133
+ handler: (client, args) => client.getOne(`/events/${args.id}`, "event"),
134
+ },
135
+ {
136
+ name: "list_milestones",
137
+ description: "List all milestones (project TaskLists) for the configured account.",
138
+ inputSchema: {
139
+ type: "object",
140
+ properties: {
141
+ project_id: { type: "integer", description: "Filter to a single project." },
142
+ complete: { type: "boolean", description: "Filter by completion." },
143
+ },
144
+ },
145
+ handler: (client, args) => client.fetchAll("/milestones", "milestones", { project_id: args.project_id, complete: args.complete }),
146
+ },
147
+ {
148
+ name: "get_milestone",
149
+ description: "Get a single milestone by id.",
150
+ inputSchema: { type: "object", properties: { id: { type: "integer" } }, required: ["id"] },
151
+ handler: (client, args) => client.getOne(`/milestones/${args.id}`, "milestone"),
152
+ },
153
+ {
154
+ name: "get_project",
155
+ description: "Get a single project by id.",
156
+ inputSchema: { type: "object", properties: { id: { type: "integer" } }, required: ["id"] },
157
+ handler: (client, args) => client.getOne(`/active_account/projects/${args.id}`, "project"),
158
+ },
159
+ // ── Tax returns (Australian company / sole-trader) ──────────────────
160
+ {
161
+ name: "list_tax_returns",
162
+ description: "List the account's tax returns (most recent financial year first) with status, completion %, and pending-findings count. Australian accounts only.",
163
+ inputSchema: { type: "object", properties: {} },
164
+ handler: (client) => client.getList("/tax_returns", "tax_returns"),
165
+ },
166
+ {
167
+ name: "get_tax_return",
168
+ description: "Get one tax return in full: every section with its fields, current values, the source of each value (auto / manual / calculated), whether each field is editable, and the calculated taxable income and tax. Read this before updating a field.",
169
+ inputSchema: {
170
+ type: "object",
171
+ properties: { id: { type: "integer", description: "Tax return id." } },
172
+ required: ["id"],
173
+ },
174
+ handler: (client, args) => client.getOne(`/tax_returns/${args.id}`, "tax_return"),
175
+ },
176
+ {
177
+ name: "get_tax_return_field_transactions",
178
+ description: "Show the chart-of-accounts transactions behind a single tax-return figure (the drill-down for an income or expense field), grouped by account, each with its journal entry number. Returns drillable:false for calculated or manually-entered fields.",
179
+ inputSchema: {
180
+ type: "object",
181
+ properties: {
182
+ id: { type: "integer", description: "Tax return id." },
183
+ section_key: { type: "string", description: "e.g. item_6_income, item_6_expenses." },
184
+ field_key: { type: "string", description: "e.g. other_sales_goods_services, motor_vehicle." },
185
+ },
186
+ required: ["id", "section_key", "field_key"],
187
+ },
188
+ handler: (client, args) => client.getOne(`/tax_returns/${args.id}/sections/${args.section_key}/fields/${args.field_key}/transactions`, ""),
189
+ },
190
+ {
191
+ name: "list_tax_return_findings",
192
+ description: "List the AI review findings for a tax return. Optionally filter by status (pending, accepted, rejected, deferred, stale).",
193
+ inputSchema: {
194
+ type: "object",
195
+ properties: {
196
+ id: { type: "integer", description: "Tax return id." },
197
+ status: { type: "string", description: "Optional status filter." },
198
+ },
199
+ required: ["id"],
200
+ },
201
+ handler: (client, args) => client.getList(`/tax_returns/${args.id}/findings`, "findings", { status: args.status }),
202
+ },
203
+ {
204
+ name: "update_tax_return_field",
205
+ description: "Set a manual override on one tax-return field — the same as a person editing it in the web form. Recomputes the totals and marks any now-stale AI findings. Pass an empty value to clear an override and revert to the auto-filled figure. Fails on calculated/read-only fields and on finalised or lodged returns. Requires a token with the tax_returns:write scope.",
206
+ inputSchema: {
207
+ type: "object",
208
+ properties: {
209
+ id: { type: "integer", description: "Tax return id." },
210
+ section_key: { type: "string", description: "Section key, e.g. item_6_expenses." },
211
+ field_key: { type: "string", description: "Field key, e.g. motor_vehicle." },
212
+ value: {
213
+ type: "string",
214
+ description: "New value (numbers as strings, e.g. '15154'). An empty string clears the override.",
215
+ },
216
+ },
217
+ required: ["id", "section_key", "field_key", "value"],
218
+ },
219
+ handler: (client, args) => client.mutate("PATCH", `/tax_returns/${args.id}/field`, {
220
+ section_key: args.section_key,
221
+ field_key: args.field_key,
222
+ value: args.value,
223
+ }),
224
+ },
225
+ {
226
+ name: "confirm_tax_return_section",
227
+ description: "Mark a tax-return section as confirmed (reviewed). Requires a token with the tax_returns:write scope.",
228
+ inputSchema: {
229
+ type: "object",
230
+ properties: {
231
+ id: { type: "integer", description: "Tax return id." },
232
+ section_key: { type: "string", description: "Section key to confirm." },
233
+ },
234
+ required: ["id", "section_key"],
235
+ },
236
+ handler: (client, args) => client.mutate("POST", `/tax_returns/${args.id}/sections/${args.section_key}/confirm`, {}),
237
+ },
238
+ // ── Manual / general journal entries (RawJournalEntry) ──────────────────
239
+ {
240
+ name: "list_manual_journals",
241
+ description: "List manual (general) journal entries for the account, newest first. Optional state filter ('draft' or 'finalised'). Requires journal_entries:read (write implies read).",
242
+ inputSchema: {
243
+ type: "object",
244
+ properties: {
245
+ state: { type: "string", enum: ["draft", "finalised"], description: "Optional state filter." },
246
+ },
247
+ },
248
+ handler: (client, args) => client.fetchAll("/raw_journal_entries", "raw_journal_entries", { state: args.state }),
249
+ },
250
+ {
251
+ name: "get_manual_journal",
252
+ description: "Get a single manual journal entry by id, including its debit/credit lines. Requires journal_entries:read.",
253
+ inputSchema: {
254
+ type: "object",
255
+ properties: { id: { type: "integer", description: "Manual journal entry id." } },
256
+ required: ["id"],
257
+ },
258
+ handler: (client, args) => client.getOne(`/raw_journal_entries/${args.id}`, "raw_journal_entry"),
259
+ },
260
+ {
261
+ name: "create_manual_journal",
262
+ description: "Create a manual (general) journal entry — the double-entry primitive behind every accounting adjustment that is not an invoice or expense (depreciation, accruals, asset disposals, reclassifications, opening balances, tax provisions, dividends). " +
263
+ "Finalised (posted to the ledger) on create by default, like every other Beeswax journal. Pass status: \"draft\" to stage it for review instead (then finalise_manual_journal). " +
264
+ "Rules enforced by the API: two or more lines; each line has account_id (a chart-of-accounts transaction_account id) and exactly one of debit or credit as a positive number; total debits MUST equal total credits; every account must belong to this account. " +
265
+ "Requires a token with the journal_entries:write scope.",
266
+ inputSchema: {
267
+ type: "object",
268
+ properties: {
269
+ date: { type: "string", description: "Entry date, e.g. '2024-04-01'." },
270
+ narration: { type: "string", description: "What the journal is for, e.g. 'BMW write-off'." },
271
+ status: { type: "string", enum: ["finalised", "draft"], description: "Optional. Default 'finalised' posts to the ledger immediately; 'draft' stages it for review." },
272
+ lines: {
273
+ type: "array",
274
+ description: "Two or more balanced lines; total debits must equal total credits.",
275
+ items: {
276
+ type: "object",
277
+ properties: {
278
+ account_id: { type: "integer", description: "Chart-of-accounts transaction_account id (the account to debit or credit)." },
279
+ debit: { type: "number", description: "Positive debit amount. Omit or 0 on a credit line." },
280
+ credit: { type: "number", description: "Positive credit amount. Omit or 0 on a debit line." },
281
+ description: { type: "string", description: "Line narration." },
282
+ },
283
+ required: ["account_id"],
284
+ },
285
+ },
286
+ },
287
+ required: ["date", "narration", "lines"],
288
+ },
289
+ handler: (client, args) => client.mutate("POST", "/raw_journal_entries", {
290
+ manual_journal: {
291
+ date: normalizeDate(args.date),
292
+ narration: args.narration,
293
+ status: args.status,
294
+ lines: args.lines,
295
+ },
296
+ }),
297
+ },
298
+ {
299
+ name: "finalise_manual_journal",
300
+ description: "Post a DRAFT manual journal to the ledger (draft -> finalised) and allocate its number. This is the deliberate, human-in-the-loop step that makes the entry real — only call it once the draft has been reviewed and confirmed. Requires journal_entries:write.",
301
+ inputSchema: {
302
+ type: "object",
303
+ properties: { id: { type: "integer", description: "Manual journal entry id (must be a draft)." } },
304
+ required: ["id"],
305
+ },
306
+ handler: (client, args) => client.mutate("POST", `/raw_journal_entries/${args.id}/finalise`, {}),
307
+ },
308
+ ];
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "beeswax-mcp",
3
+ "version": "0.1.0",
4
+ "description": "Official MCP server for Beeswax (beeswaxapp.com) — query invoices, quotes, expenses, payments, journals, time entries, projects and tax returns from Claude and other MCP clients.",
5
+ "license": "MIT",
6
+ "homepage": "https://www.beeswaxapp.com/support/mcp-setup",
7
+ "bugs": {
8
+ "url": "https://www.beeswaxapp.com/support"
9
+ },
10
+ "keywords": [
11
+ "mcp",
12
+ "modelcontextprotocol",
13
+ "beeswax",
14
+ "accounting",
15
+ "bookkeeping",
16
+ "invoices",
17
+ "claude"
18
+ ],
19
+ "type": "module",
20
+ "bin": {
21
+ "beeswax-mcp": "dist/index.js"
22
+ },
23
+ "main": "dist/index.js",
24
+ "files": [
25
+ "dist"
26
+ ],
27
+ "engines": {
28
+ "node": ">=18"
29
+ },
30
+ "scripts": {
31
+ "build": "tsc",
32
+ "start": "node dist/index.js",
33
+ "dev": "tsc --watch",
34
+ "prepublishOnly": "npm run build"
35
+ },
36
+ "dependencies": {
37
+ "@modelcontextprotocol/sdk": "^1.0.0"
38
+ },
39
+ "devDependencies": {
40
+ "@types/node": "^20.0.0",
41
+ "typescript": "^5.5.0"
42
+ }
43
+ }