@alfe.ai/myob-mcp 0.4.3 → 0.5.1

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.
Files changed (2) hide show
  1. package/dist/server.js +124 -2
  2. package/package.json +2 -2
package/dist/server.js CHANGED
@@ -94,6 +94,27 @@ var MyobClient = class {
94
94
  */
95
95
  const myobBusinessIdField = z.string().min(1).describe("MYOB Business ID — use the value from myob_list_businesses to pick which connected business this call should target.");
96
96
  const myobResourceUidField = z.string().min(1).describe("MYOB resource UID (GUID)");
97
+ /**
98
+ * Report date selector. MYOB report endpoints take dates as query values, so
99
+ * keep this a plain string: `MyobClient.get` only accepts Record<string,string>
100
+ * and the Pattern A guard classifies non-ZodString shapes as "non-string".
101
+ * Date-only `YYYY-MM-DD` matches MYOB's own report examples and is URL-safe.
102
+ */
103
+ const myobDateField = z.string().regex(/^\d{4}-\d{2}-\d{2}$/u, "date must be YYYY-MM-DD").refine((value) => {
104
+ const [y, m, d] = value.split("-").map(Number);
105
+ const date = new Date(Date.UTC(y, m - 1, d));
106
+ return date.getUTCFullYear() === y && date.getUTCMonth() === m - 1 && date.getUTCDate() === d;
107
+ }, { message: "date must be a real calendar date (YYYY-MM-DD)" }).describe("Date in YYYY-MM-DD format (e.g. 2026-07-01)");
108
+ /**
109
+ * Boolean report option. LLMs stringify booleans as readily as numbers and the
110
+ * MCP SDK validates the live schema before the handler, so accept "true"/"false"
111
+ * too. NOT `z.coerce.boolean()`: `Boolean("false") === true`.
112
+ */
113
+ const myobBooleanField = z.union([z.boolean(), z.enum(["true", "false"]).transform((value) => value === "true")]);
114
+ /** Model-visible guard for an inverted report period. */
115
+ function assertReportPeriod(startDate, endDate) {
116
+ if (startDate > endDate) throw new Error(`startDate (${startDate}) is after endDate (${endDate}). MYOB would silently fall back to a default period; swap the dates or narrow the range and retry.`);
117
+ }
97
118
  function myobResourcePath(collectionPath, uid) {
98
119
  return `${collectionPath}/${encodeURIComponent(uid)}`;
99
120
  }
@@ -115,6 +136,64 @@ const updateToolAnnotations = {
115
136
  idempotentHint: false,
116
137
  openWorldHint: true
117
138
  };
139
+ /** MYOB's maximum page size; report endpoints default to 400 rows. */
140
+ const MYOB_REPORT_PAGE_SIZE = "1000";
141
+ function isReportPage(value) {
142
+ return typeof value === "object" && value !== null && Array.isArray(value.AccountsBreakdown);
143
+ }
144
+ /**
145
+ * Fetch every page of a `/Report/*Summary` endpoint and return one merged
146
+ * report. MYOB pages `AccountsBreakdown` (400 rows by default, 1,000 max) and
147
+ * signals more rows with an absolute `NextPageLink`; the `$skip`/`$top` on
148
+ * that link are re-issued against the same path so the client's base URL and
149
+ * auth headers stay in charge. A report cut short by the page cap carries
150
+ * `Incomplete: true` and the unread `NextPageLink` instead of silently
151
+ * dropping accounts.
152
+ */
153
+ async function getReportAllPages(client, path, params) {
154
+ const first = await client.get(path, {
155
+ ...params,
156
+ $top: MYOB_REPORT_PAGE_SIZE
157
+ });
158
+ if (!isReportPage(first)) return first;
159
+ const merged = {
160
+ ...first,
161
+ AccountsBreakdown: [...first.AccountsBreakdown]
162
+ };
163
+ let page = first;
164
+ let pages = 1;
165
+ while (page.NextPageLink !== void 0 && page.NextPageLink !== null) {
166
+ const incomplete = {
167
+ ...merged,
168
+ Incomplete: true,
169
+ NextPageLink: page.NextPageLink
170
+ };
171
+ if (pages >= 20) return incomplete;
172
+ if (typeof page.NextPageLink !== "string" || page.NextPageLink.length === 0) return incomplete;
173
+ let skip;
174
+ try {
175
+ skip = new URL(page.NextPageLink).searchParams.get("$skip");
176
+ } catch {
177
+ return incomplete;
178
+ }
179
+ if (!skip) return incomplete;
180
+ const next = await client.get(path, {
181
+ ...params,
182
+ $top: MYOB_REPORT_PAGE_SIZE,
183
+ $skip: skip
184
+ });
185
+ if (!isReportPage(next)) return {
186
+ ...merged,
187
+ Incomplete: true,
188
+ NextPageLink: page.NextPageLink
189
+ };
190
+ merged.AccountsBreakdown.push(...next.AccountsBreakdown);
191
+ page = next;
192
+ pages += 1;
193
+ }
194
+ delete merged.NextPageLink;
195
+ return merged;
196
+ }
118
197
  //#endregion
119
198
  //#region src/tools/contacts.ts
120
199
  function registerContactTools(server, resolveClient) {
@@ -162,7 +241,7 @@ function registerContactTools(server, resolveClient) {
162
241
  CompanyName: z.string().optional().describe("Company name"),
163
242
  FirstName: z.string().optional().describe("First name"),
164
243
  LastName: z.string().optional().describe("Last name"),
165
- IsIndividual: z.boolean().optional().describe("true for individual, false for company"),
244
+ IsIndividual: myobBooleanField.optional().describe("true for individual, false for company"),
166
245
  DisplayID: z.string().optional().describe("Display ID (account number)"),
167
246
  data: z.record(z.string(), z.unknown()).optional().describe("Full customer data object — overrides individual fields if provided")
168
247
  }
@@ -238,7 +317,7 @@ function registerContactTools(server, resolveClient) {
238
317
  CompanyName: z.string().optional().describe("Company name"),
239
318
  FirstName: z.string().optional().describe("First name"),
240
319
  LastName: z.string().optional().describe("Last name"),
241
- IsIndividual: z.boolean().optional().describe("true for individual, false for company"),
320
+ IsIndividual: myobBooleanField.optional().describe("true for individual, false for company"),
242
321
  DisplayID: z.string().optional().describe("Display ID"),
243
322
  data: z.record(z.string(), z.unknown()).optional().describe("Full supplier data object")
244
323
  }
@@ -538,6 +617,49 @@ function registerGeneralLedgerTools(server, resolveClient) {
538
617
  text: JSON.stringify(result, null, 2)
539
618
  }] };
540
619
  });
620
+ register("myob_get_profit_and_loss", {
621
+ description: "Profit and loss summary for a connected MYOB Business over a date range. Returns one total per income/expense account (AccountsBreakdown), NOT individual transactions — use this instead of listing invoices or journal entries to answer profitability or monthly P&L questions.",
622
+ annotations: readOnlyToolAnnotations,
623
+ inputSchema: {
624
+ myobBusinessId: myobBusinessIdField,
625
+ startDate: myobDateField.describe("First day of the reporting period (YYYY-MM-DD)"),
626
+ endDate: myobDateField.describe("Last day of the reporting period (YYYY-MM-DD)"),
627
+ reportingBasis: z.enum(["Accrual", "Cash"]).optional().describe("Accrual (default) recognises income when invoiced; Cash when paid"),
628
+ yearEndAdjust: myobBooleanField.optional().describe("Include year-end adjustment entries. Defaults to false")
629
+ }
630
+ }, async (args) => {
631
+ assertReportPeriod(args.startDate, args.endDate);
632
+ const result = await getReportAllPages(resolveClient(args.myobBusinessId), "/Report/ProfitAndLossSummary", {
633
+ StartDate: args.startDate,
634
+ EndDate: args.endDate,
635
+ ReportingBasis: args.reportingBasis ?? "Accrual",
636
+ YearEndAdjust: String(args.yearEndAdjust ?? false)
637
+ });
638
+ return { content: [{
639
+ type: "text",
640
+ text: JSON.stringify(result, null, 2)
641
+ }] };
642
+ });
643
+ register("myob_get_balance_sheet", {
644
+ description: "Balance sheet summary for a connected MYOB Business as at a single date. Returns one total per asset/liability/equity account (AccountsBreakdown), NOT individual transactions.",
645
+ annotations: readOnlyToolAnnotations,
646
+ inputSchema: {
647
+ myobBusinessId: myobBusinessIdField,
648
+ asOfDate: myobDateField.describe("Balance date (YYYY-MM-DD)"),
649
+ reportingBasis: z.enum(["Accrual", "Cash"]).optional().describe("Accrual (default) or Cash"),
650
+ yearEndAdjust: myobBooleanField.optional().describe("Include year-end adjustment entries. Defaults to false")
651
+ }
652
+ }, async (args) => {
653
+ const result = await getReportAllPages(resolveClient(args.myobBusinessId), "/Report/BalanceSheetSummary", {
654
+ AsOfDate: args.asOfDate,
655
+ ReportingBasis: args.reportingBasis ?? "Accrual",
656
+ YearEndAdjust: String(args.yearEndAdjust ?? false)
657
+ });
658
+ return { content: [{
659
+ type: "text",
660
+ text: JSON.stringify(result, null, 2)
661
+ }] };
662
+ });
541
663
  register("myob_list_tax_codes", {
542
664
  description: "List tax codes from a connected MYOB Business (read-only for MYOB Essentials).",
543
665
  annotations: readOnlyToolAnnotations,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alfe.ai/myob-mcp",
3
- "version": "0.4.3",
3
+ "version": "0.5.1",
4
4
  "description": "MYOB Business MCP server — direct API integration with Alfe OAuth credentials and automatic token refresh",
5
5
  "type": "module",
6
6
  "main": "./dist/server.js",
@@ -21,7 +21,7 @@
21
21
  "zod": "^4.0.5",
22
22
  "@alfe.ai/agent-api-client": "0.19.2",
23
23
  "@alfe.ai/config": "0.4.1",
24
- "@alfe.ai/mcp-bundler": "0.4.2"
24
+ "@alfe.ai/mcp-bundler": "0.4.3"
25
25
  },
26
26
  "license": "UNLICENSED",
27
27
  "homepage": "https://alfe.ai",