beeswax-mcp 1.6.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
@@ -154,6 +154,12 @@ reports the same on demand.
154
154
 
155
155
  ### Notes
156
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
+
157
163
  - **Scopes.** `query_journal_entries` / `get_journal_entry` need an `all`-scoped
158
164
  token for a bare listing, a show, or a long-tail type (credits, bank_transfers,
159
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",
@@ -553,13 +573,13 @@ export const TOOLS = [
553
573
  "Long-tail types (credits, bank_transfers, payrolls) and bare listings require an `all`-scoped token.",
554
574
  inputSchema: {
555
575
  type: "object",
556
- properties: {
576
+ properties: { ...SYNC_PROPS,
557
577
  type: { type: "string", description: "STI type filter, e.g. invoices, payments, payrolls, credits, bank_transfers." },
558
578
  state: { type: "string", description: "Filter by state (switches the posted default off)." },
559
579
  ...POSTED_PROPS,
560
580
  },
561
581
  },
562
- handler: (client, args) => client.fetchAll("/journal_entries", "journal_entries", {
582
+ handler: (client, args) => client.fetchAll("/journal_entries", "journal_entries", { updated_since: args.updated_since,
563
583
  type: args.type, state: args.state, posted: args.posted, include_drafts: args.include_drafts,
564
584
  }),
565
585
  },
@@ -583,12 +603,12 @@ export const TOOLS = [
583
603
  description: "List all time entries for the configured account (every page).",
584
604
  inputSchema: {
585
605
  type: "object",
586
- properties: {
606
+ properties: { ...SYNC_PROPS,
587
607
  project_id: { type: "integer", description: "Filter to a single project." },
588
608
  billable: { type: "boolean", description: "Filter by billable flag." },
589
609
  },
590
610
  },
591
- 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 }),
592
612
  },
593
613
  {
594
614
  name: "get_time_entry",
@@ -601,9 +621,9 @@ export const TOOLS = [
601
621
  description: "List calendar events visible to the token's user (private events the user is not invited to are excluded).",
602
622
  inputSchema: {
603
623
  type: "object",
604
- 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 },
605
625
  },
606
- handler: (client, args) => client.fetchAll("/events", "events", {
626
+ handler: (client, args) => client.fetchAll("/events", "events", { updated_since: args.updated_since,
607
627
  project_id: args.project_id,
608
628
  from: normalizeDate(args.from),
609
629
  to: normalizeDate(args.to),
@@ -620,12 +640,12 @@ export const TOOLS = [
620
640
  description: "List all milestones (project TaskLists) for the configured account.",
621
641
  inputSchema: {
622
642
  type: "object",
623
- properties: {
643
+ properties: { ...SYNC_PROPS,
624
644
  project_id: { type: "integer", description: "Filter to a single project." },
625
645
  complete: { type: "boolean", description: "Filter by completion." },
626
646
  },
627
647
  },
628
- 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 }),
629
649
  },
630
650
  {
631
651
  name: "get_milestone",
@@ -636,8 +656,8 @@ export const TOOLS = [
636
656
  {
637
657
  name: "list_projects",
638
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.",
639
- inputSchema: { type: "object", properties: {} },
640
- 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 }),
641
661
  },
642
662
  {
643
663
  name: "get_project",
@@ -668,12 +688,12 @@ export const TOOLS = [
668
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').",
669
689
  inputSchema: {
670
690
  type: "object",
671
- properties: {
691
+ properties: { ...SYNC_PROPS,
672
692
  project_id: { type: "integer", description: "Only documents on this project" },
673
693
  document_type: { type: "string", description: "e.g. 'freeform' for markdown documents" },
674
694
  },
675
695
  },
676
- handler: (client, args) => client.fetchAll("/project_documents", "project_documents", {
696
+ handler: (client, args) => client.fetchAll("/project_documents", "project_documents", { updated_since: args.updated_since,
677
697
  project_id: args.project_id,
678
698
  document_type: args.document_type,
679
699
  }),
@@ -689,7 +709,7 @@ export const TOOLS = [
689
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.",
690
710
  inputSchema: {
691
711
  type: "object",
692
- properties: {
712
+ properties: { ...IDEMPOTENCY_PROP,
693
713
  project_id: { type: "integer" },
694
714
  name: { type: "string", description: "Document title (defaults to 'Untitled document')" },
695
715
  body: { type: "string", description: "Markdown content" },
@@ -699,7 +719,7 @@ export const TOOLS = [
699
719
  },
700
720
  handler: (client, args) => client.mutate("POST", "/project_documents", {
701
721
  project_document: { project_id: args.project_id, name: args.name, body: args.body, note: args.note },
702
- }),
722
+ }, { idempotencyKey: args.idempotency_key }),
703
723
  },
704
724
  {
705
725
  name: "update_project_document",
@@ -829,7 +849,7 @@ export const TOOLS = [
829
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.",
830
850
  inputSchema: {
831
851
  type: "object",
832
- properties: {
852
+ properties: { ...SYNC_PROPS,
833
853
  kind: {
834
854
  type: "string",
835
855
  enum: ["sell", "buy", "buy_and_sell", "product", "service"],
@@ -849,7 +869,7 @@ export const TOOLS = [
849
869
  },
850
870
  },
851
871
  },
852
- handler: (client, args) => client.fetchAll("/transaction_templates", "transaction_templates", {
872
+ handler: (client, args) => client.fetchAll("/transaction_templates", "transaction_templates", { updated_since: args.updated_since,
853
873
  kind: args.kind,
854
874
  side: args.side,
855
875
  active: args.active,
@@ -876,7 +896,7 @@ export const TOOLS = [
876
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.",
877
897
  inputSchema: {
878
898
  type: "object",
879
- properties: {
899
+ properties: { ...IDEMPOTENCY_PROP,
880
900
  title: { type: "string", description: "Catalogue name, e.g. 'Shaker Door — Painted'. Must be unique in the account." },
881
901
  kind: {
882
902
  type: "string",
@@ -930,7 +950,7 @@ export const TOOLS = [
930
950
  physical_resource: args.physical_resource,
931
951
  gantt_color: args.gantt_color,
932
952
  },
933
- }),
953
+ }, { idempotencyKey: args.idempotency_key }),
934
954
  },
935
955
  {
936
956
  name: "update_product_service",
@@ -1266,12 +1286,12 @@ export const TOOLS = [
1266
1286
  "Requires journal_entries:read (write implies read).",
1267
1287
  inputSchema: {
1268
1288
  type: "object",
1269
- properties: {
1289
+ properties: { ...SYNC_PROPS,
1270
1290
  state: { type: "string", enum: ["draft", "finalised"], description: "Optional state filter (switches the posted default off)." },
1271
1291
  ...POSTED_PROPS,
1272
1292
  },
1273
1293
  },
1274
- 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,
1275
1295
  state: args.state, posted: args.posted, include_drafts: args.include_drafts,
1276
1296
  }),
1277
1297
  },
@@ -1293,9 +1313,9 @@ export const TOOLS = [
1293
1313
  "Requires a token with the journal_entries:write scope.",
1294
1314
  inputSchema: {
1295
1315
  type: "object",
1296
- properties: {
1316
+ properties: { ...IDEMPOTENCY_PROP,
1297
1317
  date: { type: "string", description: "Entry date, e.g. '2024-04-01'." },
1298
- 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." },
1299
1319
  status: { type: "string", enum: ["finalised", "draft"], description: "Optional. Default 'finalised' posts to the ledger immediately; 'draft' stages it for review." },
1300
1320
  lines: {
1301
1321
  type: "array",
@@ -1321,7 +1341,7 @@ export const TOOLS = [
1321
1341
  status: args.status,
1322
1342
  lines: args.lines,
1323
1343
  },
1324
- }),
1344
+ }, { idempotencyKey: args.idempotency_key }),
1325
1345
  },
1326
1346
  {
1327
1347
  name: "finalise_manual_journal",
@@ -1344,7 +1364,7 @@ export const TOOLS = [
1344
1364
  "Requires a token with transaction_accounts:read.",
1345
1365
  inputSchema: {
1346
1366
  type: "object",
1347
- properties: {
1367
+ properties: { ...SYNC_PROPS,
1348
1368
  account_type: {
1349
1369
  type: "string",
1350
1370
  enum: ["income", "expense", "asset", "loan", "equity", "transfer"],
@@ -1354,7 +1374,7 @@ export const TOOLS = [
1354
1374
  bank: { type: "boolean", description: "true returns only bank accounts." },
1355
1375
  },
1356
1376
  },
1357
- 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,
1358
1378
  account_type: args.account_type,
1359
1379
  active: args.active,
1360
1380
  bank: args.bank,
@@ -1404,12 +1424,12 @@ export const TOOLS = [
1404
1424
  "Fetches every page. Requires a token with companies:read.",
1405
1425
  inputSchema: {
1406
1426
  type: "object",
1407
- properties: {
1427
+ properties: { ...SYNC_PROPS,
1408
1428
  role: { type: "string", enum: ["client", "supplier"], description: "Filter to one role. A quote is addressed to a client." },
1409
1429
  name: { type: "string", description: "Case-insensitive substring match on the company name." },
1410
1430
  },
1411
1431
  },
1412
- 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 }),
1413
1433
  },
1414
1434
  {
1415
1435
  name: "get_company",
@@ -1424,7 +1444,7 @@ export const TOOLS = [
1424
1444
  "Check list_companies first — names are unique per account and a duplicate is refused. Nothing here emails anyone. Requires companies:write.",
1425
1445
  inputSchema: {
1426
1446
  type: "object",
1427
- properties: {
1447
+ properties: { ...IDEMPOTENCY_PROP,
1428
1448
  name: { type: "string" },
1429
1449
  client: { type: "boolean", description: "You sell to them." },
1430
1450
  supplier: { type: "boolean", description: "You buy from them." },
@@ -1451,7 +1471,7 @@ export const TOOLS = [
1451
1471
  },
1452
1472
  required: ["name"],
1453
1473
  },
1454
- handler: (client, args) => client.mutate("POST", "/active_account/companies", { company: args }),
1474
+ handler: (client, args) => client.mutate("POST", "/active_account/companies", { company: args }, { idempotencyKey: args.idempotency_key }),
1455
1475
  },
1456
1476
  {
1457
1477
  name: "update_company",
@@ -1508,7 +1528,7 @@ export const TOOLS = [
1508
1528
  "Requires a token with the quotes:write scope.",
1509
1529
  inputSchema: {
1510
1530
  type: "object",
1511
- properties: {
1531
+ properties: { ...IDEMPOTENCY_PROP,
1512
1532
  project_id: { type: "integer", description: "Project the quote belongs to. Required." },
1513
1533
  company_id: { type: "integer", description: "Client the quote is addressed to. Required — see list_companies." },
1514
1534
  title: { type: "string", description: "Quote title, e.g. 'Cambridge Market Admin App Website'." },
@@ -1574,7 +1594,7 @@ export const TOOLS = [
1574
1594
  start_on: normalizeDate(group.start_on),
1575
1595
  })),
1576
1596
  },
1577
- }),
1597
+ }, { idempotencyKey: args.idempotency_key }),
1578
1598
  },
1579
1599
  {
1580
1600
  name: "update_quote",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "beeswax-mcp",
3
- "version": "1.6.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",