beeswax-mcp 1.5.0 → 1.7.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` |
@@ -143,7 +144,7 @@ reports the same on demand.
143
144
  | `list_transaction_accounts` | `/active_account/transaction_accounts` |
144
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) |
145
146
  | `list_taxes` | `/active_account/taxes` |
146
- | `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 |
147
148
  | `list_task_files`, `list_task_file_versions` | `/tasks/:id/files` (+ per-file version history) |
148
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) |
149
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) |
@@ -153,6 +154,12 @@ reports the same on demand.
153
154
 
154
155
  ### Notes
155
156
 
157
+ - The create tools and `apply_payment` accept `idempotency_key`. Reuse it on a
158
+ retry and the API replays the original result instead of posting twice.
159
+ - Every `list_*` tool accepts `updated_since` (ISO 8601) so an agent can ask
160
+ for "what changed since yesterday" instead of re-reading everything; rows
161
+ carry `updated_at`.
162
+
156
163
  - **Scopes.** `query_journal_entries` / `get_journal_entry` need an `all`-scoped
157
164
  token for a bare listing, a show, or a long-tail type (credits, bank_transfers,
158
165
  payrolls); a typed listing (`type: "invoices"`) accepts that type's read scope.
package/dist/client.js CHANGED
@@ -124,18 +124,23 @@ export class BeeswaxClient {
124
124
  }
125
125
  // Write request (POST / PATCH / DELETE) with a JSON body. Returns the parsed
126
126
  // response body, or throws with the API's error message on a non-2xx.
127
- async mutate(method, path, body = {}) {
127
+ async mutate(method, path, body = {}, opts = {}) {
128
128
  const url = new URL(this.baseUrl + path);
129
+ const headers = {
130
+ Authorization: `Bearer ${this.token}`,
131
+ Accept: "application/json",
132
+ "Content-Type": "application/json",
133
+ "User-Agent": USER_AGENT,
134
+ };
135
+ // Idempotency-Key: the API replays the original response for a retried
136
+ // write instead of posting twice (see the API tokens help).
137
+ if (opts.idempotencyKey)
138
+ headers["Idempotency-Key"] = String(opts.idempotencyKey).slice(0, 255);
129
139
  let res;
130
140
  try {
131
141
  res = await fetch(url, {
132
142
  method,
133
- headers: {
134
- Authorization: `Bearer ${this.token}`,
135
- Accept: "application/json",
136
- "Content-Type": "application/json",
137
- "User-Agent": USER_AGENT,
138
- },
143
+ headers,
139
144
  body: JSON.stringify(body),
140
145
  });
141
146
  }
package/dist/tools.js CHANGED
@@ -38,7 +38,26 @@ const DATE_PROPS = {
38
38
  },
39
39
  to: { type: "string", description: "Latest document date (sent_on). Same format as `from`." },
40
40
  };
41
+ // Optional on every create/apply tool: reuse the same key when retrying a call
42
+ // that may have gone through (timeout, unclear error) and the API returns the
43
+ // original result instead of creating a second record.
44
+ const IDEMPOTENCY_PROP = {
45
+ idempotency_key: {
46
+ type: "string",
47
+ description: "Optional. A unique string (e.g. a UUID) for this operation. If you retry the same call after a timeout or an unclear error, pass the SAME key and Beeswax returns the original result instead of creating a duplicate. Use a new key for a genuinely new record.",
48
+ },
49
+ };
50
+ // Incremental sync: every listing accepts updated_since (ISO 8601) and the
51
+ // API echoes meta.filter.updated_since + meta.server_time.
52
+ const SYNC_PROPS = {
53
+ updated_since: {
54
+ type: "string",
55
+ description: "Incremental sync: only rows updated on or after this ISO 8601 instant (e.g. 2026-09-05T00:00:00Z). " +
56
+ "Line and contact-person edits touch their parent, so a changed line surfaces the document.",
57
+ },
58
+ };
41
59
  const ACCOUNTING_LIST_PROPS = {
60
+ ...SYNC_PROPS,
42
61
  state: { type: "string", description: "Filter by state (draft, finalised, pdf_sent, partial_paid, paid, …)." },
43
62
  project_id: { type: "integer", description: "Filter to a single project." },
44
63
  company_id: { type: "integer", description: "Filter to a single client/supplier company by id." },
@@ -98,6 +117,7 @@ function accountingTools(resource, singular, label, listName, opts = {}) {
98
117
  (payable ? " Pass overdue:true to get just the overdue ones (with due dates) in a single call." : ""),
99
118
  inputSchema: { type: "object", properties: { ...listProps, ...POSTED_PROPS } },
100
119
  handler: (client, args) => client.fetchAll(`/${resource}`, resource, {
120
+ updated_since: args.updated_since,
101
121
  posted: args.posted,
102
122
  include_drafts: args.include_drafts,
103
123
  state: args.state,
@@ -182,7 +202,7 @@ function ledgerDocumentTools(resource) {
182
202
  `Requires ${scope}.`,
183
203
  inputSchema: {
184
204
  type: "object",
185
- properties: {
205
+ properties: { ...IDEMPOTENCY_PROP,
186
206
  project_id: { type: "integer", description: "Project the document belongs to. Required." },
187
207
  ...partyProp,
188
208
  title: { type: "string" },
@@ -230,7 +250,7 @@ function ledgerDocumentTools(resource) {
230
250
  default_tax_id: args.default_tax_id,
231
251
  groups: (args.groups ?? []).map((g) => ({ ...g, due_on: normalizeDate(g.due_on) })),
232
252
  },
233
- }),
253
+ }, { idempotencyKey: args.idempotency_key }),
234
254
  },
235
255
  {
236
256
  name: `update_${singular}`,
@@ -411,7 +431,7 @@ const PAYMENT_TOOLS = [
411
431
  "There is no edit: remove a wrong payment (remove_payment) and apply it again. Confirm amounts with the user first. Requires payments:write.",
412
432
  inputSchema: {
413
433
  type: "object",
414
- properties: {
434
+ properties: { ...IDEMPOTENCY_PROP,
415
435
  bank_account_id: { type: "integer", description: "A transaction account flagged as a bank account." },
416
436
  paid_on: { type: "string", description: "Payment date, e.g. '2026-09-04'." },
417
437
  allocations: {
@@ -431,7 +451,7 @@ const PAYMENT_TOOLS = [
431
451
  },
432
452
  handler: (client, args) => client.mutate("POST", "/payments", {
433
453
  payment: { bank_account_id: args.bank_account_id, paid_on: normalizeDate(args.paid_on), allocations: args.allocations },
434
- }),
454
+ }, { idempotencyKey: args.idempotency_key }),
435
455
  },
436
456
  {
437
457
  name: "remove_payment",
@@ -441,6 +461,71 @@ const PAYMENT_TOOLS = [
441
461
  handler: (client, args) => client.mutate("DELETE", `/payments/${args.id}`),
442
462
  },
443
463
  ];
464
+ // Which tool groups a token's scopes leave unusable. `all` unlocks everything;
465
+ // a :write scope implies its :read. Surfaced by check_beeswax_connection so a
466
+ // "403 on list_transaction_accounts" is explained before it happens.
467
+ const SCOPE_TOOL_GROUPS = [
468
+ { 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
+ { scope: "transactions:read", tools: "list_*_transactions, list_journal_entry_transactions, list_account_transactions", why: "ledger postings" },
470
+ { scope: "companies:read", tools: "list_companies, get_company", why: "resolving a client or supplier by name" },
471
+ { scope: "projects:read", tools: "list_projects, get_project", why: "resolving a project by name" },
472
+ { scope: "invoices:read", tools: "list_invoices, get_invoice, list_invoice_versions", why: "" },
473
+ { scope: "expenses:read", tools: "list_expenses, get_expense, list_expense_versions", why: "" },
474
+ { scope: "payments:read", tools: "query_payments, get_payment", why: "" },
475
+ { scope: "journal_entries:read", tools: "list_manual_journals, get_manual_journal", why: "" },
476
+ ];
477
+ function scopeGaps(scopes) {
478
+ if (scopes.includes("all"))
479
+ return [];
480
+ const has = (scope) => scopes.includes(scope) || scopes.includes(scope.replace(/:read$/, ":write"));
481
+ return SCOPE_TOOL_GROUPS.filter((g) => !has(g.scope)).map((g) => ({
482
+ missing_scope: g.scope,
483
+ blocks: g.tools,
484
+ ...(g.why ? { why: g.why } : {}),
485
+ }));
486
+ }
487
+ // ── Attachments on documents ─────────────────────────────────────────────
488
+ // Files ride along inline as base64 — an MCP client has no other way to send
489
+ // bytes — so this is for receipts and signed documents, not bulk transfer.
490
+ function attachmentTools(resource) {
491
+ const singular = resource.slice(0, -1);
492
+ const idKey = `${singular}_id`;
493
+ const scope = `${resource}:write`;
494
+ return [
495
+ {
496
+ name: `list_${singular}_files`,
497
+ description: `List the files attached to a ${singular} (receipts, purchase orders, signed documents), newest first, with download URLs. Requires ${resource}:read.`,
498
+ inputSchema: { type: "object", properties: { [idKey]: { type: "integer" } }, required: [idKey] },
499
+ handler: (client, args) => client.getList(`/${resource}/${args[idKey]}/files`, "files"),
500
+ },
501
+ {
502
+ name: `attach_${singular}_file`,
503
+ 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. ` +
504
+ `The file counts towards the account's storage quota. A voided ${singular} is refused. Requires ${scope}.`,
505
+ inputSchema: {
506
+ type: "object",
507
+ properties: {
508
+ [idKey]: { type: "integer" },
509
+ name: { type: "string", description: "File name with extension, e.g. receipt.pdf." },
510
+ content_type: { type: "string", description: "MIME type; detected from the content if omitted." },
511
+ base64: { type: "string", description: "The file's bytes, base64-encoded." },
512
+ description: { type: "string" },
513
+ },
514
+ required: [idKey, "name", "base64"],
515
+ },
516
+ handler: (client, args) => client.mutate("POST", `/${resource}/${args[idKey]}/files`, {
517
+ description: args.description,
518
+ file: { name: args.name, content_type: args.content_type, base64: args.base64 },
519
+ }),
520
+ },
521
+ {
522
+ name: `delete_${singular}_file`,
523
+ description: `Delete a file attached to a ${singular}. The uploader, or a manager of the document. Confirm with the user first. Requires ${scope}.`,
524
+ inputSchema: { type: "object", properties: { [idKey]: { type: "integer" }, file_id: { type: "integer" } }, required: [idKey, "file_id"] },
525
+ handler: (client, args) => client.mutate("DELETE", `/${resource}/${args[idKey]}/files/${args.file_id}`),
526
+ },
527
+ ];
528
+ }
444
529
  export const TOOLS = [
445
530
  {
446
531
  name: "check_beeswax_connection",
@@ -462,6 +547,7 @@ export const TOOLS = [
462
547
  account: meta?.account,
463
548
  user: meta?.user,
464
549
  token_scopes: meta?.scopes,
550
+ scope_gaps: scopeGaps(meta?.scopes ?? []),
465
551
  server_time: meta?.server_time,
466
552
  api_base_url: client.origin,
467
553
  };
@@ -474,6 +560,9 @@ export const TOOLS = [
474
560
  ...ledgerDocumentTools("invoices"),
475
561
  ...ledgerDocumentTools("expenses"),
476
562
  ...PAYMENT_TOOLS,
563
+ ...attachmentTools("invoices"),
564
+ ...attachmentTools("expenses"),
565
+ ...attachmentTools("quotes"),
477
566
  // Generic journal-entries reader for the long tail.
478
567
  {
479
568
  name: "query_journal_entries",
@@ -484,13 +573,13 @@ export const TOOLS = [
484
573
  "Long-tail types (credits, bank_transfers, payrolls) and bare listings require an `all`-scoped token.",
485
574
  inputSchema: {
486
575
  type: "object",
487
- properties: {
576
+ properties: { ...SYNC_PROPS,
488
577
  type: { type: "string", description: "STI type filter, e.g. invoices, payments, payrolls, credits, bank_transfers." },
489
578
  state: { type: "string", description: "Filter by state (switches the posted default off)." },
490
579
  ...POSTED_PROPS,
491
580
  },
492
581
  },
493
- handler: (client, args) => client.fetchAll("/journal_entries", "journal_entries", {
582
+ handler: (client, args) => client.fetchAll("/journal_entries", "journal_entries", { updated_since: args.updated_since,
494
583
  type: args.type, state: args.state, posted: args.posted, include_drafts: args.include_drafts,
495
584
  }),
496
585
  },
@@ -514,12 +603,12 @@ export const TOOLS = [
514
603
  description: "List all time entries for the configured account (every page).",
515
604
  inputSchema: {
516
605
  type: "object",
517
- properties: {
606
+ properties: { ...SYNC_PROPS,
518
607
  project_id: { type: "integer", description: "Filter to a single project." },
519
608
  billable: { type: "boolean", description: "Filter by billable flag." },
520
609
  },
521
610
  },
522
- handler: (client, args) => client.fetchAll("/time_entries", "time_entries", { project_id: args.project_id, billable: args.billable }),
611
+ handler: (client, args) => client.fetchAll("/time_entries", "time_entries", { updated_since: args.updated_since, project_id: args.project_id, billable: args.billable }),
523
612
  },
524
613
  {
525
614
  name: "get_time_entry",
@@ -532,9 +621,9 @@ export const TOOLS = [
532
621
  description: "List calendar events visible to the token's user (private events the user is not invited to are excluded).",
533
622
  inputSchema: {
534
623
  type: "object",
535
- properties: { project_id: { type: "integer", description: "Filter to a single project." }, ...DATE_PROPS },
624
+ properties: { ...SYNC_PROPS, project_id: { type: "integer", description: "Filter to a single project." }, ...DATE_PROPS },
536
625
  },
537
- handler: (client, args) => client.fetchAll("/events", "events", {
626
+ handler: (client, args) => client.fetchAll("/events", "events", { updated_since: args.updated_since,
538
627
  project_id: args.project_id,
539
628
  from: normalizeDate(args.from),
540
629
  to: normalizeDate(args.to),
@@ -551,12 +640,12 @@ export const TOOLS = [
551
640
  description: "List all milestones (project TaskLists) for the configured account.",
552
641
  inputSchema: {
553
642
  type: "object",
554
- properties: {
643
+ properties: { ...SYNC_PROPS,
555
644
  project_id: { type: "integer", description: "Filter to a single project." },
556
645
  complete: { type: "boolean", description: "Filter by completion." },
557
646
  },
558
647
  },
559
- handler: (client, args) => client.fetchAll("/milestones", "milestones", { project_id: args.project_id, complete: args.complete }),
648
+ handler: (client, args) => client.fetchAll("/milestones", "milestones", { updated_since: args.updated_since, project_id: args.project_id, complete: args.complete }),
560
649
  },
561
650
  {
562
651
  name: "get_milestone",
@@ -567,8 +656,8 @@ export const TOOLS = [
567
656
  {
568
657
  name: "list_projects",
569
658
  description: "List the account's active projects. Use this to resolve a project name to the project_id that create_quote and the other document tools need.",
570
- inputSchema: { type: "object", properties: {} },
571
- handler: (client) => client.getList("/active_account/projects", "projects"),
659
+ inputSchema: { type: "object", properties: { ...SYNC_PROPS } },
660
+ handler: (client, args) => client.getList("/active_account/projects", "projects", { updated_since: args.updated_since }),
572
661
  },
573
662
  {
574
663
  name: "get_project",
@@ -599,12 +688,12 @@ export const TOOLS = [
599
688
  description: "List project documents. Freeform documents are markdown written in Beeswax's document editor — proposals, briefs, meeting notes — styled by the account's invoice theme; other document_types are generated from library templates and are read-only via this API. Each row carries state (draft/published/finalized/archived), current_version_number, and editing_lock (who has it open in the web editor right now). Filter with project_id and/or document_type (e.g. 'freeform').",
600
689
  inputSchema: {
601
690
  type: "object",
602
- properties: {
691
+ properties: { ...SYNC_PROPS,
603
692
  project_id: { type: "integer", description: "Only documents on this project" },
604
693
  document_type: { type: "string", description: "e.g. 'freeform' for markdown documents" },
605
694
  },
606
695
  },
607
- handler: (client, args) => client.fetchAll("/project_documents", "project_documents", {
696
+ handler: (client, args) => client.fetchAll("/project_documents", "project_documents", { updated_since: args.updated_since,
608
697
  project_id: args.project_id,
609
698
  document_type: args.document_type,
610
699
  }),
@@ -620,7 +709,7 @@ export const TOOLS = [
620
709
  description: "Create a freeform (markdown) document on a project. `body` is markdown (headings, lists, tables, blockquotes all render through the account's invoice theme). The optional `note` becomes the first version's 'what changed' line. Requires a token with the `project_documents:write` scope.",
621
710
  inputSchema: {
622
711
  type: "object",
623
- properties: {
712
+ properties: { ...IDEMPOTENCY_PROP,
624
713
  project_id: { type: "integer" },
625
714
  name: { type: "string", description: "Document title (defaults to 'Untitled document')" },
626
715
  body: { type: "string", description: "Markdown content" },
@@ -630,7 +719,7 @@ export const TOOLS = [
630
719
  },
631
720
  handler: (client, args) => client.mutate("POST", "/project_documents", {
632
721
  project_document: { project_id: args.project_id, name: args.name, body: args.body, note: args.note },
633
- }),
722
+ }, { idempotencyKey: args.idempotency_key }),
634
723
  },
635
724
  {
636
725
  name: "update_project_document",
@@ -760,7 +849,7 @@ export const TOOLS = [
760
849
  description: "List the account's products & services catalogue (stored as 'transaction templates'): everything the business sells or buys as a line item — products and services/activities with their unit, sell/buy prices, linked income/expense accounts and taxes. Use this whenever the user asks about products, services, price lists, rates or catalogue items.",
761
850
  inputSchema: {
762
851
  type: "object",
763
- properties: {
852
+ properties: { ...SYNC_PROPS,
764
853
  kind: {
765
854
  type: "string",
766
855
  enum: ["sell", "buy", "buy_and_sell", "product", "service"],
@@ -780,7 +869,7 @@ export const TOOLS = [
780
869
  },
781
870
  },
782
871
  },
783
- handler: (client, args) => client.fetchAll("/transaction_templates", "transaction_templates", {
872
+ handler: (client, args) => client.fetchAll("/transaction_templates", "transaction_templates", { updated_since: args.updated_since,
784
873
  kind: args.kind,
785
874
  side: args.side,
786
875
  active: args.active,
@@ -807,7 +896,7 @@ export const TOOLS = [
807
896
  "Titles must be unique within the account, and prices default to 0 if you omit them, so pass them explicitly. Requires a token with transaction_templates:write.",
808
897
  inputSchema: {
809
898
  type: "object",
810
- properties: {
899
+ properties: { ...IDEMPOTENCY_PROP,
811
900
  title: { type: "string", description: "Catalogue name, e.g. 'Shaker Door — Painted'. Must be unique in the account." },
812
901
  kind: {
813
902
  type: "string",
@@ -861,7 +950,7 @@ export const TOOLS = [
861
950
  physical_resource: args.physical_resource,
862
951
  gantt_color: args.gantt_color,
863
952
  },
864
- }),
953
+ }, { idempotencyKey: args.idempotency_key }),
865
954
  },
866
955
  {
867
956
  name: "update_product_service",
@@ -1197,12 +1286,12 @@ export const TOOLS = [
1197
1286
  "Requires journal_entries:read (write implies read).",
1198
1287
  inputSchema: {
1199
1288
  type: "object",
1200
- properties: {
1289
+ properties: { ...SYNC_PROPS,
1201
1290
  state: { type: "string", enum: ["draft", "finalised"], description: "Optional state filter (switches the posted default off)." },
1202
1291
  ...POSTED_PROPS,
1203
1292
  },
1204
1293
  },
1205
- handler: (client, args) => client.fetchAll("/raw_journal_entries", "raw_journal_entries", {
1294
+ handler: (client, args) => client.fetchAll("/raw_journal_entries", "raw_journal_entries", { updated_since: args.updated_since,
1206
1295
  state: args.state, posted: args.posted, include_drafts: args.include_drafts,
1207
1296
  }),
1208
1297
  },
@@ -1224,9 +1313,9 @@ export const TOOLS = [
1224
1313
  "Requires a token with the journal_entries:write scope.",
1225
1314
  inputSchema: {
1226
1315
  type: "object",
1227
- properties: {
1316
+ properties: { ...IDEMPOTENCY_PROP,
1228
1317
  date: { type: "string", description: "Entry date, e.g. '2024-04-01'." },
1229
- narration: { type: "string", description: "What the journal is for, e.g. 'BMW write-off'." },
1318
+ narration: { type: "string", maxLength: 255, description: "What the journal is for, e.g. 'BMW write-off'. At most 255 characters — put detail in the line descriptions." },
1230
1319
  status: { type: "string", enum: ["finalised", "draft"], description: "Optional. Default 'finalised' posts to the ledger immediately; 'draft' stages it for review." },
1231
1320
  lines: {
1232
1321
  type: "array",
@@ -1252,7 +1341,7 @@ export const TOOLS = [
1252
1341
  status: args.status,
1253
1342
  lines: args.lines,
1254
1343
  },
1255
- }),
1344
+ }, { idempotencyKey: args.idempotency_key }),
1256
1345
  },
1257
1346
  {
1258
1347
  name: "finalise_manual_journal",
@@ -1275,7 +1364,7 @@ export const TOOLS = [
1275
1364
  "Requires a token with transaction_accounts:read.",
1276
1365
  inputSchema: {
1277
1366
  type: "object",
1278
- properties: {
1367
+ properties: { ...SYNC_PROPS,
1279
1368
  account_type: {
1280
1369
  type: "string",
1281
1370
  enum: ["income", "expense", "asset", "loan", "equity", "transfer"],
@@ -1285,7 +1374,7 @@ export const TOOLS = [
1285
1374
  bank: { type: "boolean", description: "true returns only bank accounts." },
1286
1375
  },
1287
1376
  },
1288
- handler: (client, args) => client.getList("/active_account/transaction_accounts", "transaction_accounts", {
1377
+ handler: (client, args) => client.getList("/active_account/transaction_accounts", "transaction_accounts", { updated_since: args.updated_since,
1289
1378
  account_type: args.account_type,
1290
1379
  active: args.active,
1291
1380
  bank: args.bank,
@@ -1335,12 +1424,93 @@ export const TOOLS = [
1335
1424
  "Fetches every page. Requires a token with companies:read.",
1336
1425
  inputSchema: {
1337
1426
  type: "object",
1338
- properties: {
1427
+ properties: { ...SYNC_PROPS,
1339
1428
  role: { type: "string", enum: ["client", "supplier"], description: "Filter to one role. A quote is addressed to a client." },
1340
1429
  name: { type: "string", description: "Case-insensitive substring match on the company name." },
1341
1430
  },
1342
1431
  },
1343
- handler: (client, args) => client.fetchAll("/active_account/companies", "companies", { role: args.role, name: args.name }),
1432
+ handler: (client, args) => client.fetchAll("/active_account/companies", "companies", { updated_since: args.updated_since, role: args.role, name: args.name }),
1433
+ },
1434
+ {
1435
+ name: "get_company",
1436
+ description: "Get one client or supplier by id, with its contact people. Requires companies:read.",
1437
+ inputSchema: { type: "object", properties: { id: { type: "integer" } }, required: ["id"] },
1438
+ handler: (client, args) => client.getOne(`/active_account/companies/${args.id}`, "company"),
1439
+ },
1440
+ {
1441
+ name: "create_company",
1442
+ 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. " +
1443
+ "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. " +
1444
+ "Check list_companies first — names are unique per account and a duplicate is refused. Nothing here emails anyone. Requires companies:write.",
1445
+ inputSchema: {
1446
+ type: "object",
1447
+ properties: { ...IDEMPOTENCY_PROP,
1448
+ name: { type: "string" },
1449
+ client: { type: "boolean", description: "You sell to them." },
1450
+ supplier: { type: "boolean", description: "You buy from them." },
1451
+ phone: { type: "string" },
1452
+ address: { type: "string" },
1453
+ city: { type: "string" },
1454
+ state: { type: "string" },
1455
+ postcode: { type: "string" },
1456
+ country: { type: "string" },
1457
+ web_address: { type: "string" },
1458
+ invoice_details: { type: "string", description: "Free text printed on documents: ABN / tax number, reference lines." },
1459
+ default_tax_id: { type: "integer", description: "See list_taxes." },
1460
+ people: {
1461
+ type: "array",
1462
+ items: {
1463
+ type: "object",
1464
+ properties: {
1465
+ first_name: { type: "string" }, last_name: { type: "string" }, email: { type: "string" },
1466
+ phone: { type: "string" }, mobile: { type: "string" },
1467
+ },
1468
+ required: ["first_name", "last_name", "email"],
1469
+ },
1470
+ },
1471
+ },
1472
+ required: ["name"],
1473
+ },
1474
+ handler: (client, args) => client.mutate("POST", "/active_account/companies", { company: args }, { idempotencyKey: args.idempotency_key }),
1475
+ },
1476
+ {
1477
+ name: "update_company",
1478
+ 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. " +
1479
+ "A company must remain a client, a supplier, or both. The business's own company and system companies are refused. Requires companies:write.",
1480
+ inputSchema: {
1481
+ type: "object",
1482
+ properties: {
1483
+ id: { type: "integer" },
1484
+ name: { type: "string" },
1485
+ client: { type: "boolean" },
1486
+ supplier: { type: "boolean" },
1487
+ phone: { type: "string" },
1488
+ address: { type: "string" },
1489
+ city: { type: "string" },
1490
+ state: { type: "string" },
1491
+ postcode: { type: "string" },
1492
+ country: { type: "string" },
1493
+ web_address: { type: "string" },
1494
+ invoice_details: { type: "string" },
1495
+ default_tax_id: { type: ["integer", "null"] },
1496
+ people: {
1497
+ type: "array",
1498
+ items: {
1499
+ type: "object",
1500
+ properties: {
1501
+ first_name: { type: "string" }, last_name: { type: "string" }, email: { type: "string" },
1502
+ phone: { type: "string" }, mobile: { type: "string" },
1503
+ },
1504
+ required: ["first_name", "last_name", "email"],
1505
+ },
1506
+ },
1507
+ },
1508
+ required: ["id"],
1509
+ },
1510
+ handler: (client, args) => {
1511
+ const { id, ...body } = args;
1512
+ return client.mutate("PATCH", `/active_account/companies/${id}`, { company: body });
1513
+ },
1344
1514
  },
1345
1515
  // ── Quote authoring ──────────────────────────────────────────────────
1346
1516
  // The boundary here is deliberate: create and edit, never send. There is no
@@ -1358,7 +1528,7 @@ export const TOOLS = [
1358
1528
  "Requires a token with the quotes:write scope.",
1359
1529
  inputSchema: {
1360
1530
  type: "object",
1361
- properties: {
1531
+ properties: { ...IDEMPOTENCY_PROP,
1362
1532
  project_id: { type: "integer", description: "Project the quote belongs to. Required." },
1363
1533
  company_id: { type: "integer", description: "Client the quote is addressed to. Required — see list_companies." },
1364
1534
  title: { type: "string", description: "Quote title, e.g. 'Cambridge Market Admin App Website'." },
@@ -1424,7 +1594,7 @@ export const TOOLS = [
1424
1594
  start_on: normalizeDate(group.start_on),
1425
1595
  })),
1426
1596
  },
1427
- }),
1597
+ }, { idempotencyKey: args.idempotency_key }),
1428
1598
  },
1429
1599
  {
1430
1600
  name: "update_quote",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "beeswax-mcp",
3
- "version": "1.5.0",
3
+ "version": "1.7.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",