@alfe.ai/myob-mcp 0.4.3 → 0.5.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/dist/server.js +124 -2
- package/package.json +1 -1
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:
|
|
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:
|
|
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