beeswax-mcp 1.4.0 → 1.6.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
@@ -134,6 +134,7 @@ reports the same on demand.
134
134
  | `list_expenses`, `get_expense`, `list_expense_transactions` | `/expenses` |
135
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` … |
136
136
  | `query_payments`, `get_payment`, `list_payment_transactions` | `/payments` |
137
+ | `list_invoice_files`, `attach_invoice_file` (write), `delete_invoice_file` (write), and the same for expenses and quotes | `/invoices/:id/files` … — attachments; the file rides inline as base64 (25 MB cap) |
137
138
  | `apply_payment` (write), `remove_payment` (write) | `/payments` — one payment per allocated document, one money side per call, all-or-nothing; no update |
138
139
  | `query_journal_entries`, `get_journal_entry`, `list_journal_entry_transactions` | `/journal_entries` |
139
140
  | `list_time_entries`, `get_time_entry` | `/time_entries` |
@@ -141,8 +142,9 @@ reports the same on demand.
141
142
  | `list_milestones`, `get_milestone` | `/milestones` |
142
143
  | `list_projects`, `get_project` | `/active_account/projects` |
143
144
  | `list_transaction_accounts` | `/active_account/transaction_accounts` |
145
+ | `list_account_transactions` | `/active_account/transaction_accounts/:id/transactions?from=&to=` — one account's posted ledger for a period with opening/running/closing balances (the reconciliation query) |
144
146
  | `list_taxes` | `/active_account/taxes` |
145
- | `list_companies` | `/active_account/companies` |
147
+ | `list_companies`, `get_company`, `create_company` (write), `update_company` (write) | `/active_account/companies` — clients and suppliers with their contact people; a company must be a client, a supplier or both |
146
148
  | `list_task_files`, `list_task_file_versions` | `/tasks/:id/files` (+ per-file version history) |
147
149
  | `list_project_documents`, `get_project_document`, `create_project_document` (write), `update_project_document` (write), `list_project_document_versions`, `get_project_document_version`, `restore_project_document_version` (write) | `/project_documents` (+ per-document version history) |
148
150
  | `list_themes`, `get_theme_spec`, `get_theme`, `upsert_theme` (write), `validate_theme` (write), `preview_theme`, `activate_theme` (write), `delete_theme` (write) | `/themes` (custom document themes, THEME_SPEC v1) |
package/dist/client.js CHANGED
@@ -84,6 +84,28 @@ export class BeeswaxClient {
84
84
  transactions: body?.transactions ?? [],
85
85
  };
86
86
  }
87
+ // Paginated list whose envelope matters (balances, totals): walks every
88
+ // page, merges the array under rootKey, and keeps the first page's other
89
+ // top-level fields (minus meta).
90
+ async fetchAllWithEnvelope(path, rootKey, params = {}) {
91
+ const perPage = 100;
92
+ let page = 1;
93
+ let totalPages = 1;
94
+ let envelope = null;
95
+ const all = [];
96
+ do {
97
+ const body = await this.request(path, { ...params, page, per_page: perPage });
98
+ if (!envelope) {
99
+ envelope = { ...body };
100
+ delete envelope[rootKey];
101
+ delete envelope.meta;
102
+ }
103
+ all.push(...(body?.[rootKey] ?? []));
104
+ totalPages = Number(body?.meta?.total_pages ?? 1) || 1;
105
+ page += 1;
106
+ } while (page <= totalPages && all.length < 20000);
107
+ return { ...(envelope ?? {}), [rootKey]: all };
108
+ }
87
109
  // Paginated list — walks every page (via meta.total_pages) and returns the
88
110
  // merged array. Capped defensively so a malformed meta can't loop forever.
89
111
  async fetchAll(path, rootKey, params = {}) {
package/dist/tools.js CHANGED
@@ -441,6 +441,71 @@ const PAYMENT_TOOLS = [
441
441
  handler: (client, args) => client.mutate("DELETE", `/payments/${args.id}`),
442
442
  },
443
443
  ];
444
+ // Which tool groups a token's scopes leave unusable. `all` unlocks everything;
445
+ // a :write scope implies its :read. Surfaced by check_beeswax_connection so a
446
+ // "403 on list_transaction_accounts" is explained before it happens.
447
+ const SCOPE_TOOL_GROUPS = [
448
+ { scope: "transaction_accounts:read", tools: "list_transaction_accounts, list_taxes, list_account_transactions", why: "chart of accounts, tax codes and the per-account ledger — needed for any reconciliation and for pricing document lines" },
449
+ { scope: "transactions:read", tools: "list_*_transactions, list_journal_entry_transactions, list_account_transactions", why: "ledger postings" },
450
+ { scope: "companies:read", tools: "list_companies, get_company", why: "resolving a client or supplier by name" },
451
+ { scope: "projects:read", tools: "list_projects, get_project", why: "resolving a project by name" },
452
+ { scope: "invoices:read", tools: "list_invoices, get_invoice, list_invoice_versions", why: "" },
453
+ { scope: "expenses:read", tools: "list_expenses, get_expense, list_expense_versions", why: "" },
454
+ { scope: "payments:read", tools: "query_payments, get_payment", why: "" },
455
+ { scope: "journal_entries:read", tools: "list_manual_journals, get_manual_journal", why: "" },
456
+ ];
457
+ function scopeGaps(scopes) {
458
+ if (scopes.includes("all"))
459
+ return [];
460
+ const has = (scope) => scopes.includes(scope) || scopes.includes(scope.replace(/:read$/, ":write"));
461
+ return SCOPE_TOOL_GROUPS.filter((g) => !has(g.scope)).map((g) => ({
462
+ missing_scope: g.scope,
463
+ blocks: g.tools,
464
+ ...(g.why ? { why: g.why } : {}),
465
+ }));
466
+ }
467
+ // ── Attachments on documents ─────────────────────────────────────────────
468
+ // Files ride along inline as base64 — an MCP client has no other way to send
469
+ // bytes — so this is for receipts and signed documents, not bulk transfer.
470
+ function attachmentTools(resource) {
471
+ const singular = resource.slice(0, -1);
472
+ const idKey = `${singular}_id`;
473
+ const scope = `${resource}:write`;
474
+ return [
475
+ {
476
+ name: `list_${singular}_files`,
477
+ description: `List the files attached to a ${singular} (receipts, purchase orders, signed documents), newest first, with download URLs. Requires ${resource}:read.`,
478
+ inputSchema: { type: "object", properties: { [idKey]: { type: "integer" } }, required: [idKey] },
479
+ handler: (client, args) => client.getList(`/${resource}/${args[idKey]}/files`, "files"),
480
+ },
481
+ {
482
+ name: `attach_${singular}_file`,
483
+ description: `Attach a file to a ${singular}. Send the file inline as base64 (name, content_type, base64) — keep it to receipts and documents, not large media; the API caps this at 25 MB. ` +
484
+ `The file counts towards the account's storage quota. A voided ${singular} is refused. Requires ${scope}.`,
485
+ inputSchema: {
486
+ type: "object",
487
+ properties: {
488
+ [idKey]: { type: "integer" },
489
+ name: { type: "string", description: "File name with extension, e.g. receipt.pdf." },
490
+ content_type: { type: "string", description: "MIME type; detected from the content if omitted." },
491
+ base64: { type: "string", description: "The file's bytes, base64-encoded." },
492
+ description: { type: "string" },
493
+ },
494
+ required: [idKey, "name", "base64"],
495
+ },
496
+ handler: (client, args) => client.mutate("POST", `/${resource}/${args[idKey]}/files`, {
497
+ description: args.description,
498
+ file: { name: args.name, content_type: args.content_type, base64: args.base64 },
499
+ }),
500
+ },
501
+ {
502
+ name: `delete_${singular}_file`,
503
+ description: `Delete a file attached to a ${singular}. The uploader, or a manager of the document. Confirm with the user first. Requires ${scope}.`,
504
+ inputSchema: { type: "object", properties: { [idKey]: { type: "integer" }, file_id: { type: "integer" } }, required: [idKey, "file_id"] },
505
+ handler: (client, args) => client.mutate("DELETE", `/${resource}/${args[idKey]}/files/${args.file_id}`),
506
+ },
507
+ ];
508
+ }
444
509
  export const TOOLS = [
445
510
  {
446
511
  name: "check_beeswax_connection",
@@ -462,6 +527,7 @@ export const TOOLS = [
462
527
  account: meta?.account,
463
528
  user: meta?.user,
464
529
  token_scopes: meta?.scopes,
530
+ scope_gaps: scopeGaps(meta?.scopes ?? []),
465
531
  server_time: meta?.server_time,
466
532
  api_base_url: client.origin,
467
533
  };
@@ -474,6 +540,9 @@ export const TOOLS = [
474
540
  ...ledgerDocumentTools("invoices"),
475
541
  ...ledgerDocumentTools("expenses"),
476
542
  ...PAYMENT_TOOLS,
543
+ ...attachmentTools("invoices"),
544
+ ...attachmentTools("expenses"),
545
+ ...attachmentTools("quotes"),
477
546
  // Generic journal-entries reader for the long tail.
478
547
  {
479
548
  name: "query_journal_entries",
@@ -1291,6 +1360,26 @@ export const TOOLS = [
1291
1360
  bank: args.bank,
1292
1361
  }),
1293
1362
  },
1363
+ {
1364
+ name: "list_account_transactions",
1365
+ description: "The ledger for ONE transaction account over a period — the reconciliation query. Returns every POSTED posting to the account between from and to (inclusive, by document date), oldest first, with the opening balance at `from`, a running balance on each line, the period's debit and credit totals and the closing balance. " +
1366
+ "Drafts, payroll templates, quotes and voided entries never appear, so these figures can be compared directly with a trial balance or a tax-return field. " +
1367
+ "Balances follow the account's classification: asset/expense accounts grow with debits, income/loan/equity accounts with credits. Accrual basis only. " +
1368
+ "Use list_transaction_accounts to find the account id. Requires transaction_accounts:read AND transactions:read.",
1369
+ inputSchema: {
1370
+ type: "object",
1371
+ properties: {
1372
+ transaction_account_id: { type: "integer" },
1373
+ from: { type: "string", description: "Start date (inclusive), e.g. '2025-07-01'. Omit for the beginning of time." },
1374
+ to: { type: "string", description: "End date (inclusive), e.g. '2026-06-30'." },
1375
+ },
1376
+ required: ["transaction_account_id"],
1377
+ },
1378
+ handler: (client, args) => client.fetchAllWithEnvelope(`/active_account/transaction_accounts/${args.transaction_account_id}/transactions`, "transactions", {
1379
+ from: normalizeDate(args.from),
1380
+ to: normalizeDate(args.to),
1381
+ }),
1382
+ },
1294
1383
  {
1295
1384
  name: "list_taxes",
1296
1385
  description: "List the account's tax codes — the ids `sell_tax_id` / `buy_tax_id` on a product, and a document line's tax, have to point at. " +
@@ -1322,6 +1411,87 @@ export const TOOLS = [
1322
1411
  },
1323
1412
  handler: (client, args) => client.fetchAll("/active_account/companies", "companies", { role: args.role, name: args.name }),
1324
1413
  },
1414
+ {
1415
+ name: "get_company",
1416
+ description: "Get one client or supplier by id, with its contact people. Requires companies:read.",
1417
+ inputSchema: { type: "object", properties: { id: { type: "integer" } }, required: ["id"] },
1418
+ handler: (client, args) => client.getOne(`/active_account/companies/${args.id}`, "company"),
1419
+ },
1420
+ {
1421
+ name: "create_company",
1422
+ description: "Create a client or supplier (a company / contact) so an invoice or quote can be addressed to it without a trip to the web app. " +
1423
+ "It must be a client (you sell to them), a supplier (you buy from them), or both. `people` are optional; the first becomes the contact documents are addressed to. " +
1424
+ "Check list_companies first — names are unique per account and a duplicate is refused. Nothing here emails anyone. Requires companies:write.",
1425
+ inputSchema: {
1426
+ type: "object",
1427
+ properties: {
1428
+ name: { type: "string" },
1429
+ client: { type: "boolean", description: "You sell to them." },
1430
+ supplier: { type: "boolean", description: "You buy from them." },
1431
+ phone: { type: "string" },
1432
+ address: { type: "string" },
1433
+ city: { type: "string" },
1434
+ state: { type: "string" },
1435
+ postcode: { type: "string" },
1436
+ country: { type: "string" },
1437
+ web_address: { type: "string" },
1438
+ invoice_details: { type: "string", description: "Free text printed on documents: ABN / tax number, reference lines." },
1439
+ default_tax_id: { type: "integer", description: "See list_taxes." },
1440
+ people: {
1441
+ type: "array",
1442
+ items: {
1443
+ type: "object",
1444
+ properties: {
1445
+ first_name: { type: "string" }, last_name: { type: "string" }, email: { type: "string" },
1446
+ phone: { type: "string" }, mobile: { type: "string" },
1447
+ },
1448
+ required: ["first_name", "last_name", "email"],
1449
+ },
1450
+ },
1451
+ },
1452
+ required: ["name"],
1453
+ },
1454
+ handler: (client, args) => client.mutate("POST", "/active_account/companies", { company: args }),
1455
+ },
1456
+ {
1457
+ name: "update_company",
1458
+ description: "Update a client or supplier. Only the fields you pass change. `people` APPENDS contacts (an existing email is left alone); removing a person stays a web-app action. " +
1459
+ "A company must remain a client, a supplier, or both. The business's own company and system companies are refused. Requires companies:write.",
1460
+ inputSchema: {
1461
+ type: "object",
1462
+ properties: {
1463
+ id: { type: "integer" },
1464
+ name: { type: "string" },
1465
+ client: { type: "boolean" },
1466
+ supplier: { type: "boolean" },
1467
+ phone: { type: "string" },
1468
+ address: { type: "string" },
1469
+ city: { type: "string" },
1470
+ state: { type: "string" },
1471
+ postcode: { type: "string" },
1472
+ country: { type: "string" },
1473
+ web_address: { type: "string" },
1474
+ invoice_details: { type: "string" },
1475
+ default_tax_id: { type: ["integer", "null"] },
1476
+ people: {
1477
+ type: "array",
1478
+ items: {
1479
+ type: "object",
1480
+ properties: {
1481
+ first_name: { type: "string" }, last_name: { type: "string" }, email: { type: "string" },
1482
+ phone: { type: "string" }, mobile: { type: "string" },
1483
+ },
1484
+ required: ["first_name", "last_name", "email"],
1485
+ },
1486
+ },
1487
+ },
1488
+ required: ["id"],
1489
+ },
1490
+ handler: (client, args) => {
1491
+ const { id, ...body } = args;
1492
+ return client.mutate("PATCH", `/active_account/companies/${id}`, { company: body });
1493
+ },
1494
+ },
1325
1495
  // ── Quote authoring ──────────────────────────────────────────────────
1326
1496
  // The boundary here is deliberate: create and edit, never send. There is no
1327
1497
  // tool that emails a quote to a client or marks one accepted — those stay
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "beeswax-mcp",
3
- "version": "1.4.0",
3
+ "version": "1.6.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",