beeswax-mcp 2.0.0 → 2.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/README.md CHANGED
@@ -143,6 +143,10 @@ reports the same on demand.
143
143
  | `list_projects`, `get_project` | `/active_account/projects` |
144
144
  | `list_transaction_accounts` | `/active_account/transaction_accounts` |
145
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) |
146
+ | `list_bank_reconciliations`, `get_bank_reconciliation`, `create_bank_reconciliation` (write), `update_bank_reconciliation` (write) | `/reconciliations` — statement checks: the period, its rows (ticked or not) and the processed statement's lines; create from figures or from a local statement PDF (`file_path`) |
147
+ | `reconcile_bank_rows` (write), `unreconcile_bank_rows` (write), `match_bank_statement_line` (write), `unmatch_bank_statement_line` (write) | `/reconciliations/:id/confirm`, `/unconfirm`, `/statement_lines/:line_id/match` — tick rows against the statement and record why; each tick is pushed live to the page |
148
+ | `upload_bank_statement` (write), `get_bank_statement_status` | `/reconciliations/:id/statement` — send a statement PDF for Beeswax's own extraction and matching, then poll |
149
+ | `find_possible_duplicate_payments` | `/reconciliations/possible_duplicates` — a bill paid twice, entered twice, or the same movement twice on the statement |
146
150
  | `list_taxes` | `/active_account/taxes` |
147
151
  | `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 |
148
152
  | `list_task_files`, `list_task_file_versions` | `/tasks/:id/files` (+ per-file version history) |
@@ -154,6 +158,15 @@ reports the same on demand.
154
158
 
155
159
  ### Notes
156
160
 
161
+ - **Working a bank statement.** `get_bank_reconciliation` returns every row
162
+ Beeswax has for the period and every line of the processed statement PDF.
163
+ For each statement line, find the row it is, `reconcile_bank_rows` it and
164
+ `match_bank_statement_line` with a one-line reason; a statement line with no
165
+ row is a transaction missing from Beeswax (record it, and it appears as a
166
+ new row); a row still unticked at the end is a query for the bookkeeper.
167
+ Anyone with the reconciliation page open watches the ticks land live. Run
168
+ `find_possible_duplicate_payments` before signing off.
169
+
157
170
  - The create tools and `apply_payment` accept `idempotency_key`. Reuse it on a
158
171
  retry and the API replays the original result instead of posting twice.
159
172
  - Every `list_*` tool accepts `updated_since` (ISO 8601) so an agent can ask
package/dist/client.js CHANGED
@@ -66,6 +66,10 @@ export class BeeswaxClient {
66
66
  const body = await this.request(path);
67
67
  return rootKey ? body?.[rootKey] : body;
68
68
  }
69
+ // A whole response body, query params included (reports with several top-level keys).
70
+ async getBody(path, params = {}) {
71
+ return this.request(path, params);
72
+ }
69
73
  // Non-paginated array (e.g. an entry's transactions).
70
74
  async getList(path, rootKey, params = {}) {
71
75
  const body = await this.request(path, params);
package/dist/index.js CHANGED
@@ -29,7 +29,9 @@ const INSTRUCTIONS = "Beeswax is an accounting, project and time-tracking system
29
29
  "number or lines — never count them as transactions, wages, income or expenses. " +
30
30
  "Listings return posted entries by default; pass posted:false or include_drafts:true only when the user " +
31
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.";
32
+ "not postings. When reconciling or summing by account, use only results with posted:true. " +
33
+ "BANK STATEMENT CHECKS: tick a row (reconcile_bank_rows) only when you have found the statement line it is; " +
34
+ "a statement line with no row is a transaction missing from Beeswax; a row with no statement line stays unticked and is reported to the user.";
33
35
  const server = new Server({ name: "beeswax-mcp", version: VERSION }, { capabilities: { tools: {} }, instructions: INSTRUCTIONS });
34
36
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
35
37
  tools: TOOLS.map(({ name, description, inputSchema }) => ({ name, description, inputSchema })),
package/dist/tools.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { basename } from "node:path";
1
3
  import { normalizeDate } from "./client.js";
2
4
  import { VERSION, isNewerVersion } from "./version.js";
3
5
  // The writable half of a catalogue item. `id` is the route, not a body field,
@@ -467,6 +469,7 @@ const PAYMENT_TOOLS = [
467
469
  const SCOPE_TOOL_GROUPS = [
468
470
  { 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" },
469
471
  { scope: "transactions:read", tools: "list_*_transactions, list_journal_entry_transactions, list_account_transactions", why: "ledger postings" },
472
+ { scope: "reconciliations:read", tools: "list_bank_reconciliations, get_bank_reconciliation, get_bank_statement_status, find_possible_duplicate_payments", why: "reading a bank statement check and its rows; add reconciliations:write to tick rows, upload statements and record matches" },
470
473
  { scope: "companies:read", tools: "list_companies, get_company", why: "resolving a client or supplier by name" },
471
474
  { scope: "projects:read", tools: "list_projects, get_project", why: "resolving a project by name" },
472
475
  { scope: "invoices:read", tools: "list_invoices, get_invoice, list_invoice_versions", why: "" },
@@ -526,7 +529,205 @@ function attachmentTools(resource) {
526
529
  },
527
530
  ];
528
531
  }
532
+ // ── Bank reconciliations (statement checks) ──────────────────────────────
533
+ // The reconciliation page as tools: read the period and its rows, tick the
534
+ // rows that appear on the statement, tie statement lines to rows with a
535
+ // reason, upload the statement PDF, and ask what looks doubled up. Every
536
+ // write is pushed to the page over Turbo Streams, so a person with the page
537
+ // open watches the rows tick off as the assistant works.
538
+ const RECONCILIATION_ROW_IDS = {
539
+ remittance_ids: {
540
+ type: "array",
541
+ items: { type: "integer" },
542
+ description: "remittance_id values from get_bank_reconciliation rows (individual payments, transfers, manual journals).",
543
+ },
544
+ line_item_ids: {
545
+ type: "array",
546
+ items: { type: "integer" },
547
+ description: "line_item_id values from rows that group several payments on one bank-feed line (they tick as one).",
548
+ },
549
+ };
550
+ function readStatementFile(args) {
551
+ if (args.base64) {
552
+ return { name: args.name ?? "statement.pdf", content_type: args.content_type ?? "application/pdf", base64: args.base64 };
553
+ }
554
+ if (!args.file_path)
555
+ throw new Error("Give file_path (a local PDF) or base64.");
556
+ const path = String(args.file_path).replace(/^~(?=$|\/)/, process.env.HOME ?? "~");
557
+ const bytes = readFileSync(path);
558
+ return { name: args.name ?? basename(path), content_type: "application/pdf", base64: bytes.toString("base64") };
559
+ }
560
+ const RECONCILIATION_TOOLS = [
561
+ {
562
+ name: "list_bank_reconciliations",
563
+ description: "List bank reconciliations (statement checks): one per bank account per statement period, newest first, with the statement's opening/closing balances, what Beeswax has ticked (total_received, total_spent), the signed difference and a state: " +
564
+ "pending_statement (created from a PDF still being read), reconciled, balanced (difference zero but rows still unticked) or unreconciled. " +
565
+ "Use list_transaction_accounts (bank accounts only) to find bank_account_id. Requires reconciliations:read.",
566
+ inputSchema: {
567
+ type: "object",
568
+ properties: {
569
+ bank_account_id: { type: "integer" },
570
+ state: { type: "string", enum: ["pending_statement", "reconciled", "balanced", "unreconciled"] },
571
+ ...SYNC_PROPS,
572
+ },
573
+ },
574
+ handler: (client, args) => client.getList("/reconciliations", "reconciliations", { bank_account_id: args.bank_account_id, state: args.state, updated_since: args.updated_since }),
575
+ },
576
+ {
577
+ name: "get_bank_reconciliation",
578
+ description: "The whole reconciliation page in one call: the header, every ROW of the table (a Beeswax transaction on the bank account in the period, ticked or not, with received/spent, the document and what it pays for), row_counts, and the STATEMENT LINES of the most recently processed statement PDF with what each was matched to (status unmatched = nothing in Beeswax for it). " +
579
+ "A row is addressed by remittance_id, or by line_item_id when several payments sit on one bank-feed line and tick together. " +
580
+ "Reconciling means: for each statement line find the row it is, tick the row (reconcile_bank_rows) and record the reason (match_bank_statement_line); a statement line with no row is a transaction missing from Beeswax — record it (create_expense/apply_payment) and it becomes a new row; a row still unticked after the whole statement is walked is a query for the bookkeeper, never something to force. " +
581
+ "web_url is the page a person can watch while you work. Requires reconciliations:read.",
582
+ inputSchema: { type: "object", properties: { id: { type: "integer" } }, required: ["id"] },
583
+ handler: (client, args) => client.getOne(`/reconciliations/${args.id}`, ""),
584
+ },
585
+ {
586
+ name: "create_bank_reconciliation",
587
+ description: "Create a statement check for a bank account. Either give the statement period and balances (start_on, finish_on, opening_balance, closing_balance) to start ticking immediately, or give file_path (a local statement PDF) and Beeswax extracts the period and balances and runs its own matcher first — the result is pending_statement until get_bank_statement_status reports completed. " +
588
+ "Requires reconciliations:write and an owner or accountant role.",
589
+ inputSchema: {
590
+ type: "object",
591
+ properties: { ...IDEMPOTENCY_PROP,
592
+ bank_account_id: { type: "integer", description: "A transaction account flagged as a bank account." },
593
+ start_on: { type: "string", description: "Statement start date, e.g. '2026-04-01'." },
594
+ finish_on: { type: "string", description: "Statement end date." },
595
+ opening_balance: { type: "number", description: "The statement's opening balance." },
596
+ closing_balance: { type: "number", description: "The statement's closing balance." },
597
+ file_path: { type: "string", description: "Local path to the statement PDF (read by this connector and sent to Beeswax). Alternative to the four period fields." },
598
+ base64: { type: "string", description: "The PDF's bytes, base64-encoded — for clients that cannot read files." },
599
+ name: { type: "string", description: "File name for the upload, e.g. 'april.pdf'." },
600
+ },
601
+ required: ["bank_account_id"],
602
+ },
603
+ handler: (client, args) => {
604
+ const body = {
605
+ reconciliation: {
606
+ bank_account_id: args.bank_account_id,
607
+ start_on: normalizeDate(args.start_on),
608
+ finish_on: normalizeDate(args.finish_on),
609
+ opening_balance: args.opening_balance,
610
+ closing_balance: args.closing_balance,
611
+ },
612
+ };
613
+ if (args.file_path || args.base64)
614
+ body.statement = readStatementFile(args);
615
+ return client.mutate("POST", "/reconciliations", body, { idempotencyKey: args.idempotency_key });
616
+ },
617
+ },
618
+ {
619
+ name: "update_bank_reconciliation",
620
+ description: "Change a statement check's period or balances (e.g. the closing balance was mistyped). The page's table is re-broadcast. Requires reconciliations:write.",
621
+ inputSchema: {
622
+ type: "object",
623
+ properties: {
624
+ id: { type: "integer" },
625
+ start_on: { type: "string" },
626
+ finish_on: { type: "string" },
627
+ opening_balance: { type: "number" },
628
+ closing_balance: { type: "number" },
629
+ },
630
+ required: ["id"],
631
+ },
632
+ handler: (client, args) => client.mutate("PATCH", `/reconciliations/${args.id}`, {
633
+ reconciliation: {
634
+ start_on: normalizeDate(args.start_on),
635
+ finish_on: normalizeDate(args.finish_on),
636
+ opening_balance: args.opening_balance,
637
+ closing_balance: args.closing_balance,
638
+ },
639
+ }),
640
+ },
641
+ {
642
+ name: "reconcile_bank_rows",
643
+ description: "Tick rows of a statement check as appearing on the bank statement. Pass remittance_ids and/or line_item_ids exactly as get_bank_reconciliation reports them. Ids that are not rows of this reconciliation come back under `unknown` and nothing is written for them. " +
644
+ "The response carries the updated rows and the reconciliation header (difference, state). Each tick is shown live to anyone viewing the page. " +
645
+ "Tick only rows you have matched to a statement line; if the statement does not show a transaction, leave it unticked and say so. Requires reconciliations:write.",
646
+ inputSchema: { type: "object", properties: { id: { type: "integer", description: "Reconciliation id." }, ...RECONCILIATION_ROW_IDS }, required: ["id"] },
647
+ handler: (client, args) => client.mutate("POST", `/reconciliations/${args.id}/confirm`, { remittance_ids: args.remittance_ids ?? [], line_item_ids: args.line_item_ids ?? [] }),
648
+ },
649
+ {
650
+ name: "unreconcile_bank_rows",
651
+ description: "Untick rows of a statement check (the inverse of reconcile_bank_rows, same arguments). Requires reconciliations:write.",
652
+ inputSchema: { type: "object", properties: { id: { type: "integer", description: "Reconciliation id." }, ...RECONCILIATION_ROW_IDS }, required: ["id"] },
653
+ handler: (client, args) => client.mutate("POST", `/reconciliations/${args.id}/unconfirm`, { remittance_ids: args.remittance_ids ?? [], line_item_ids: args.line_item_ids ?? [] }),
654
+ },
655
+ {
656
+ name: "upload_bank_statement",
657
+ description: "Upload a bank statement PDF to an existing statement check and let Beeswax extract its lines and run its own matcher (exact, amount/date, then AI). Give file_path (a local PDF; this connector reads it) or base64. Poll get_bank_statement_status until completed, then get_bank_reconciliation shows the statement lines with their matches. PDF only, 25 MB. Requires reconciliations:write.",
658
+ inputSchema: {
659
+ type: "object",
660
+ properties: {
661
+ id: { type: "integer", description: "Reconciliation id." },
662
+ file_path: { type: "string", description: "Local path to the PDF." },
663
+ base64: { type: "string", description: "Alternative to file_path." },
664
+ name: { type: "string", description: "File name, e.g. 'april.pdf'." },
665
+ },
666
+ required: ["id"],
667
+ },
668
+ handler: (client, args) => client.mutate("POST", `/reconciliations/${args.id}/statement`, { statement: readStatementFile(args) }),
669
+ },
670
+ {
671
+ name: "get_bank_statement_status",
672
+ description: "Status of the latest statement PDF run on a statement check: idle, pending, extracting, matching, completed (with matched/unmatched counts), failed (with the error) or date_mismatch. Runs usually take under a minute. Requires reconciliations:read.",
673
+ inputSchema: { type: "object", properties: { id: { type: "integer", description: "Reconciliation id." } }, required: ["id"] },
674
+ handler: (client, args) => client.getOne(`/reconciliations/${args.id}/statement_status`, ""),
675
+ },
676
+ {
677
+ name: "match_bank_statement_line",
678
+ description: "Record that a statement line (from get_bank_reconciliation's statement_lines) IS a given row, with your reasoning, and tick the row. The line then shows as matched on the page's statement log with the reason. Refused if the line is already matched to a different row — unmatch_bank_statement_line first. Requires reconciliations:write.",
679
+ inputSchema: {
680
+ type: "object",
681
+ properties: {
682
+ id: { type: "integer", description: "Reconciliation id." },
683
+ statement_line_id: { type: "integer", description: "The statement line's id." },
684
+ remittance_id: { type: "integer", description: "The row's remittance_id (for a grouped row, any of its remittance_ids)." },
685
+ reasoning: { type: "string", description: "Why this line is that row, in one sentence — shown to the bookkeeper." },
686
+ },
687
+ required: ["id", "statement_line_id", "remittance_id"],
688
+ },
689
+ handler: (client, args) => client.mutate("POST", `/reconciliations/${args.id}/statement_lines/${args.statement_line_id}/match`, { remittance_id: args.remittance_id, reasoning: args.reasoning }),
690
+ },
691
+ {
692
+ name: "unmatch_bank_statement_line",
693
+ description: "Clear a statement line's match. untick:true also unticks the row it was matched to. Requires reconciliations:write.",
694
+ inputSchema: {
695
+ type: "object",
696
+ properties: {
697
+ id: { type: "integer", description: "Reconciliation id." },
698
+ statement_line_id: { type: "integer" },
699
+ untick: { type: "boolean" },
700
+ reasoning: { type: "string" },
701
+ },
702
+ required: ["id", "statement_line_id"],
703
+ },
704
+ handler: (client, args) => client.mutate("POST", `/reconciliations/${args.id}/statement_lines/${args.statement_line_id}/unmatch`, { untick: args.untick ?? false, reasoning: args.reasoning }),
705
+ },
706
+ {
707
+ name: "find_possible_duplicate_payments",
708
+ description: "Possible double payments, three ways: `payments` (two or more payments from the same bank account for the same amount to the same company within window_days; same_document:true = one bill settled twice — the strongest signal), `documents` (two or more posted bills or invoices from the same company for the same total within the window — the same bill entered twice) and `statement` (the same amount and description twice on the processed statement PDF; only with reconciliation_id). " +
709
+ "Give reconciliation_id (its bank account and period become the scope) or bank_account_id with from/to. Suggestions only — rent is the same every month — so present each group to the user with the reason; never remove a payment on this alone. Requires reconciliations:read.",
710
+ inputSchema: {
711
+ type: "object",
712
+ properties: {
713
+ reconciliation_id: { type: "integer" },
714
+ bank_account_id: { type: "integer" },
715
+ from: { type: "string" },
716
+ to: { type: "string" },
717
+ window_days: { type: "integer", description: "Days apart that still count as a pair. Default 7, max 90." },
718
+ },
719
+ },
720
+ handler: (client, args) => client.getBody("/reconciliations/possible_duplicates", {
721
+ reconciliation_id: args.reconciliation_id,
722
+ bank_account_id: args.bank_account_id,
723
+ from: normalizeDate(args.from),
724
+ to: normalizeDate(args.to),
725
+ window_days: args.window_days,
726
+ }),
727
+ },
728
+ ];
529
729
  export const TOOLS = [
730
+ ...RECONCILIATION_TOOLS,
530
731
  {
531
732
  name: "check_beeswax_connection",
532
733
  description: "Health-check the Beeswax connection: reports the connector version you are running, the latest released version " +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "beeswax-mcp",
3
- "version": "2.0.0",
3
+ "version": "2.1.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",