@alfe.ai/myob-mcp 0.3.16 → 0.3.18
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 +7 -1
- package/dist/server.js +142 -71
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# @alfe.ai/myob-mcp
|
|
2
2
|
|
|
3
|
-
MYOB Business MCP server
|
|
3
|
+
MYOB Business MCP server with explicit multi-business selection, Alfe-managed
|
|
4
|
+
OAuth credentials, and per-Connection automatic token refresh.
|
|
4
5
|
|
|
5
6
|
Part of [**Alfe**](https://alfe.ai) — the operating system for AI agents: build, deploy, and run agents with persistent memory, identity, integrations, and channels. See the [documentation](https://docs.alfe.ai) to get started.
|
|
6
7
|
|
|
@@ -10,6 +11,11 @@ Part of [**Alfe**](https://alfe.ai) — the operating system for AI agents: buil
|
|
|
10
11
|
npm install @alfe.ai/myob-mcp
|
|
11
12
|
```
|
|
12
13
|
|
|
14
|
+
Every MYOB API tool requires the `myobBusinessId` returned by
|
|
15
|
+
`myob_list_businesses`, so reads and financial writes cannot silently target a
|
|
16
|
+
default company file. Access tokens are refreshed independently for each
|
|
17
|
+
connected business.
|
|
18
|
+
|
|
13
19
|
## Links
|
|
14
20
|
|
|
15
21
|
- 🌐 Website: <https://alfe.ai>
|
package/dist/server.js
CHANGED
|
@@ -16,6 +16,7 @@ import { assertPatternA } from "@alfe.ai/mcp-bundler";
|
|
|
16
16
|
* - x-myobapi-version: v2
|
|
17
17
|
*/
|
|
18
18
|
const MYOB_API_BASE = "https://api.myob.com/accountright";
|
|
19
|
+
const MYOB_REQUEST_TIMEOUT_MS = 3e4;
|
|
19
20
|
var MyobClient = class {
|
|
20
21
|
accessToken;
|
|
21
22
|
clientId;
|
|
@@ -31,7 +32,7 @@ var MyobClient = class {
|
|
|
31
32
|
this.accessToken = accessToken;
|
|
32
33
|
}
|
|
33
34
|
get baseUrl() {
|
|
34
|
-
return `${MYOB_API_BASE}/${this.businessId}`;
|
|
35
|
+
return `${MYOB_API_BASE}/${encodeURIComponent(this.businessId)}`;
|
|
35
36
|
}
|
|
36
37
|
headers() {
|
|
37
38
|
return {
|
|
@@ -63,7 +64,8 @@ var MyobClient = class {
|
|
|
63
64
|
const doFetch = async () => {
|
|
64
65
|
const init = {
|
|
65
66
|
method,
|
|
66
|
-
headers: this.headers()
|
|
67
|
+
headers: this.headers(),
|
|
68
|
+
signal: AbortSignal.timeout(MYOB_REQUEST_TIMEOUT_MS)
|
|
67
69
|
};
|
|
68
70
|
if (body !== void 0) init.body = JSON.stringify(body);
|
|
69
71
|
return fetch(url, init);
|
|
@@ -90,13 +92,36 @@ var MyobClient = class {
|
|
|
90
92
|
* expects a raw Zod shape (`{ field: z.string() }`), NOT a JSON-Schema
|
|
91
93
|
* object — passing JSON-Schema throws synchronously at registration time.
|
|
92
94
|
*/
|
|
93
|
-
const myobBusinessIdField = z.string().describe("MYOB Business ID — use the value from myob_list_businesses to pick which connected business this call should target.");
|
|
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
|
+
const myobResourceUidField = z.string().min(1).describe("MYOB resource UID (GUID)");
|
|
97
|
+
function myobResourcePath(collectionPath, uid) {
|
|
98
|
+
return `${collectionPath}/${encodeURIComponent(uid)}`;
|
|
99
|
+
}
|
|
100
|
+
const readOnlyToolAnnotations = {
|
|
101
|
+
readOnlyHint: true,
|
|
102
|
+
destructiveHint: false,
|
|
103
|
+
idempotentHint: true,
|
|
104
|
+
openWorldHint: true
|
|
105
|
+
};
|
|
106
|
+
const additiveToolAnnotations = {
|
|
107
|
+
readOnlyHint: false,
|
|
108
|
+
destructiveHint: false,
|
|
109
|
+
idempotentHint: false,
|
|
110
|
+
openWorldHint: true
|
|
111
|
+
};
|
|
112
|
+
const updateToolAnnotations = {
|
|
113
|
+
readOnlyHint: false,
|
|
114
|
+
destructiveHint: true,
|
|
115
|
+
idempotentHint: false,
|
|
116
|
+
openWorldHint: true
|
|
117
|
+
};
|
|
94
118
|
//#endregion
|
|
95
119
|
//#region src/tools/contacts.ts
|
|
96
120
|
function registerContactTools(server, resolveClient) {
|
|
97
121
|
const register = server.registerTool.bind(server);
|
|
98
122
|
register("myob_list_customers", {
|
|
99
123
|
description: "List customer contacts from a connected MYOB Business.",
|
|
124
|
+
annotations: readOnlyToolAnnotations,
|
|
100
125
|
inputSchema: {
|
|
101
126
|
myobBusinessId: myobBusinessIdField,
|
|
102
127
|
filter: z.string().optional().describe("OData $filter expression (e.g. \"IsActive eq true\")"),
|
|
@@ -117,12 +142,13 @@ function registerContactTools(server, resolveClient) {
|
|
|
117
142
|
});
|
|
118
143
|
register("myob_get_customer", {
|
|
119
144
|
description: "Get a specific customer contact by UID from a connected MYOB Business.",
|
|
145
|
+
annotations: readOnlyToolAnnotations,
|
|
120
146
|
inputSchema: {
|
|
121
147
|
myobBusinessId: myobBusinessIdField,
|
|
122
|
-
uid:
|
|
148
|
+
uid: myobResourceUidField.describe("The customer UID (GUID)")
|
|
123
149
|
}
|
|
124
150
|
}, async (args) => {
|
|
125
|
-
const result = await resolveClient(args.myobBusinessId).get(
|
|
151
|
+
const result = await resolveClient(args.myobBusinessId).get(myobResourcePath("/Contact/Customer", args.uid));
|
|
126
152
|
return { content: [{
|
|
127
153
|
type: "text",
|
|
128
154
|
text: JSON.stringify(result, null, 2)
|
|
@@ -130,6 +156,7 @@ function registerContactTools(server, resolveClient) {
|
|
|
130
156
|
});
|
|
131
157
|
register("myob_create_customer", {
|
|
132
158
|
description: "Create a new customer contact in a connected MYOB Business.",
|
|
159
|
+
annotations: additiveToolAnnotations,
|
|
133
160
|
inputSchema: {
|
|
134
161
|
myobBusinessId: myobBusinessIdField,
|
|
135
162
|
CompanyName: z.string().optional().describe("Company name"),
|
|
@@ -155,13 +182,14 @@ function registerContactTools(server, resolveClient) {
|
|
|
155
182
|
});
|
|
156
183
|
register("myob_update_customer", {
|
|
157
184
|
description: "Update an existing customer contact in a connected MYOB Business.",
|
|
185
|
+
annotations: updateToolAnnotations,
|
|
158
186
|
inputSchema: {
|
|
159
187
|
myobBusinessId: myobBusinessIdField,
|
|
160
|
-
uid:
|
|
188
|
+
uid: myobResourceUidField.describe("The customer UID (GUID) to update"),
|
|
161
189
|
data: z.record(z.string(), z.unknown()).describe("Customer data fields to update")
|
|
162
190
|
}
|
|
163
191
|
}, async (args) => {
|
|
164
|
-
const result = await resolveClient(args.myobBusinessId).put(
|
|
192
|
+
const result = await resolveClient(args.myobBusinessId).put(myobResourcePath("/Contact/Customer", args.uid), args.data);
|
|
165
193
|
return { content: [{
|
|
166
194
|
type: "text",
|
|
167
195
|
text: JSON.stringify(result, null, 2)
|
|
@@ -169,6 +197,7 @@ function registerContactTools(server, resolveClient) {
|
|
|
169
197
|
});
|
|
170
198
|
register("myob_list_suppliers", {
|
|
171
199
|
description: "List supplier contacts from a connected MYOB Business.",
|
|
200
|
+
annotations: readOnlyToolAnnotations,
|
|
172
201
|
inputSchema: {
|
|
173
202
|
myobBusinessId: myobBusinessIdField,
|
|
174
203
|
filter: z.string().optional().describe("OData $filter expression"),
|
|
@@ -189,12 +218,13 @@ function registerContactTools(server, resolveClient) {
|
|
|
189
218
|
});
|
|
190
219
|
register("myob_get_supplier", {
|
|
191
220
|
description: "Get a specific supplier contact by UID from a connected MYOB Business.",
|
|
221
|
+
annotations: readOnlyToolAnnotations,
|
|
192
222
|
inputSchema: {
|
|
193
223
|
myobBusinessId: myobBusinessIdField,
|
|
194
|
-
uid:
|
|
224
|
+
uid: myobResourceUidField.describe("The supplier UID (GUID)")
|
|
195
225
|
}
|
|
196
226
|
}, async (args) => {
|
|
197
|
-
const result = await resolveClient(args.myobBusinessId).get(
|
|
227
|
+
const result = await resolveClient(args.myobBusinessId).get(myobResourcePath("/Contact/Supplier", args.uid));
|
|
198
228
|
return { content: [{
|
|
199
229
|
type: "text",
|
|
200
230
|
text: JSON.stringify(result, null, 2)
|
|
@@ -202,6 +232,7 @@ function registerContactTools(server, resolveClient) {
|
|
|
202
232
|
});
|
|
203
233
|
register("myob_create_supplier", {
|
|
204
234
|
description: "Create a new supplier contact in a connected MYOB Business.",
|
|
235
|
+
annotations: additiveToolAnnotations,
|
|
205
236
|
inputSchema: {
|
|
206
237
|
myobBusinessId: myobBusinessIdField,
|
|
207
238
|
CompanyName: z.string().optional().describe("Company name"),
|
|
@@ -232,6 +263,7 @@ function registerSalesTools(server, resolveClient) {
|
|
|
232
263
|
const register = server.registerTool.bind(server);
|
|
233
264
|
register("myob_list_invoices", {
|
|
234
265
|
description: "List sale invoices from a connected MYOB Business.",
|
|
266
|
+
annotations: readOnlyToolAnnotations,
|
|
235
267
|
inputSchema: {
|
|
236
268
|
myobBusinessId: myobBusinessIdField,
|
|
237
269
|
filter: z.string().optional().describe("OData $filter expression (e.g. \"Status eq 'Open'\")"),
|
|
@@ -252,12 +284,13 @@ function registerSalesTools(server, resolveClient) {
|
|
|
252
284
|
});
|
|
253
285
|
register("myob_get_invoice", {
|
|
254
286
|
description: "Get a specific sale invoice by UID from a connected MYOB Business.",
|
|
287
|
+
annotations: readOnlyToolAnnotations,
|
|
255
288
|
inputSchema: {
|
|
256
289
|
myobBusinessId: myobBusinessIdField,
|
|
257
|
-
uid:
|
|
290
|
+
uid: myobResourceUidField.describe("The invoice UID (GUID)")
|
|
258
291
|
}
|
|
259
292
|
}, async (args) => {
|
|
260
|
-
const result = await resolveClient(args.myobBusinessId).get(
|
|
293
|
+
const result = await resolveClient(args.myobBusinessId).get(myobResourcePath("/Sale/Invoice", args.uid));
|
|
261
294
|
return { content: [{
|
|
262
295
|
type: "text",
|
|
263
296
|
text: JSON.stringify(result, null, 2)
|
|
@@ -265,6 +298,7 @@ function registerSalesTools(server, resolveClient) {
|
|
|
265
298
|
});
|
|
266
299
|
register("myob_create_invoice", {
|
|
267
300
|
description: "Create a new sale invoice in a connected MYOB Business. Provide customer, line items, and dates.",
|
|
301
|
+
annotations: additiveToolAnnotations,
|
|
268
302
|
inputSchema: {
|
|
269
303
|
myobBusinessId: myobBusinessIdField,
|
|
270
304
|
data: z.record(z.string(), z.unknown()).describe("Full invoice data object including Customer, Date, Lines, etc.")
|
|
@@ -278,6 +312,7 @@ function registerSalesTools(server, resolveClient) {
|
|
|
278
312
|
});
|
|
279
313
|
register("myob_list_quotes", {
|
|
280
314
|
description: "List sale quotes from a connected MYOB Business.",
|
|
315
|
+
annotations: readOnlyToolAnnotations,
|
|
281
316
|
inputSchema: {
|
|
282
317
|
myobBusinessId: myobBusinessIdField,
|
|
283
318
|
filter: z.string().optional().describe("OData $filter expression"),
|
|
@@ -298,6 +333,7 @@ function registerSalesTools(server, resolveClient) {
|
|
|
298
333
|
});
|
|
299
334
|
register("myob_create_quote", {
|
|
300
335
|
description: "Create a new sale quote in a connected MYOB Business.",
|
|
336
|
+
annotations: additiveToolAnnotations,
|
|
301
337
|
inputSchema: {
|
|
302
338
|
myobBusinessId: myobBusinessIdField,
|
|
303
339
|
data: z.record(z.string(), z.unknown()).describe("Full quote data object including Customer, Date, Lines, etc.")
|
|
@@ -316,6 +352,7 @@ function registerPurchaseTools(server, resolveClient) {
|
|
|
316
352
|
const register = server.registerTool.bind(server);
|
|
317
353
|
register("myob_list_bills", {
|
|
318
354
|
description: "List purchase bills from a connected MYOB Business.",
|
|
355
|
+
annotations: readOnlyToolAnnotations,
|
|
319
356
|
inputSchema: {
|
|
320
357
|
myobBusinessId: myobBusinessIdField,
|
|
321
358
|
filter: z.string().optional().describe("OData $filter expression"),
|
|
@@ -336,12 +373,13 @@ function registerPurchaseTools(server, resolveClient) {
|
|
|
336
373
|
});
|
|
337
374
|
register("myob_get_bill", {
|
|
338
375
|
description: "Get a specific purchase bill by UID from a connected MYOB Business.",
|
|
376
|
+
annotations: readOnlyToolAnnotations,
|
|
339
377
|
inputSchema: {
|
|
340
378
|
myobBusinessId: myobBusinessIdField,
|
|
341
|
-
uid:
|
|
379
|
+
uid: myobResourceUidField.describe("The bill UID (GUID)")
|
|
342
380
|
}
|
|
343
381
|
}, async (args) => {
|
|
344
|
-
const result = await resolveClient(args.myobBusinessId).get(
|
|
382
|
+
const result = await resolveClient(args.myobBusinessId).get(myobResourcePath("/Purchase/Bill", args.uid));
|
|
345
383
|
return { content: [{
|
|
346
384
|
type: "text",
|
|
347
385
|
text: JSON.stringify(result, null, 2)
|
|
@@ -349,6 +387,7 @@ function registerPurchaseTools(server, resolveClient) {
|
|
|
349
387
|
});
|
|
350
388
|
register("myob_create_bill", {
|
|
351
389
|
description: "Create a new purchase bill in a connected MYOB Business.",
|
|
390
|
+
annotations: additiveToolAnnotations,
|
|
352
391
|
inputSchema: {
|
|
353
392
|
myobBusinessId: myobBusinessIdField,
|
|
354
393
|
data: z.record(z.string(), z.unknown()).describe("Full bill data object including Supplier, Date, Lines, etc.")
|
|
@@ -367,6 +406,7 @@ function registerGeneralLedgerTools(server, resolveClient) {
|
|
|
367
406
|
const register = server.registerTool.bind(server);
|
|
368
407
|
register("myob_list_accounts", {
|
|
369
408
|
description: "List chart of accounts (ledger accounts) from a connected MYOB Business. NOT the connected businesses themselves — for that, use myob_list_businesses.",
|
|
409
|
+
annotations: readOnlyToolAnnotations,
|
|
370
410
|
inputSchema: {
|
|
371
411
|
myobBusinessId: myobBusinessIdField,
|
|
372
412
|
filter: z.string().optional().describe("OData $filter expression (e.g. \"Type eq 'Bank'\")"),
|
|
@@ -387,12 +427,13 @@ function registerGeneralLedgerTools(server, resolveClient) {
|
|
|
387
427
|
});
|
|
388
428
|
register("myob_get_account", {
|
|
389
429
|
description: "Get a specific general ledger account by UID from a connected MYOB Business.",
|
|
430
|
+
annotations: readOnlyToolAnnotations,
|
|
390
431
|
inputSchema: {
|
|
391
432
|
myobBusinessId: myobBusinessIdField,
|
|
392
|
-
uid:
|
|
433
|
+
uid: myobResourceUidField.describe("The account UID (GUID)")
|
|
393
434
|
}
|
|
394
435
|
}, async (args) => {
|
|
395
|
-
const result = await resolveClient(args.myobBusinessId).get(
|
|
436
|
+
const result = await resolveClient(args.myobBusinessId).get(myobResourcePath("/GeneralLedger/Account", args.uid));
|
|
396
437
|
return { content: [{
|
|
397
438
|
type: "text",
|
|
398
439
|
text: JSON.stringify(result, null, 2)
|
|
@@ -400,6 +441,7 @@ function registerGeneralLedgerTools(server, resolveClient) {
|
|
|
400
441
|
});
|
|
401
442
|
register("myob_list_journal_entries", {
|
|
402
443
|
description: "List journal transactions from a connected MYOB Business general ledger.",
|
|
444
|
+
annotations: readOnlyToolAnnotations,
|
|
403
445
|
inputSchema: {
|
|
404
446
|
myobBusinessId: myobBusinessIdField,
|
|
405
447
|
filter: z.string().optional().describe("OData $filter expression"),
|
|
@@ -420,6 +462,7 @@ function registerGeneralLedgerTools(server, resolveClient) {
|
|
|
420
462
|
});
|
|
421
463
|
register("myob_create_journal_entry", {
|
|
422
464
|
description: "Create a new journal transaction in a connected MYOB Business.",
|
|
465
|
+
annotations: additiveToolAnnotations,
|
|
423
466
|
inputSchema: {
|
|
424
467
|
myobBusinessId: myobBusinessIdField,
|
|
425
468
|
data: z.record(z.string(), z.unknown()).describe("Full journal transaction data object including Lines, DateOccurred, Memo, etc.")
|
|
@@ -433,6 +476,7 @@ function registerGeneralLedgerTools(server, resolveClient) {
|
|
|
433
476
|
});
|
|
434
477
|
register("myob_list_tax_codes", {
|
|
435
478
|
description: "List tax codes from a connected MYOB Business (read-only for MYOB Essentials).",
|
|
479
|
+
annotations: readOnlyToolAnnotations,
|
|
436
480
|
inputSchema: {
|
|
437
481
|
myobBusinessId: myobBusinessIdField,
|
|
438
482
|
filter: z.string().optional().describe("OData $filter expression")
|
|
@@ -454,6 +498,7 @@ function registerInventoryTools(server, resolveClient) {
|
|
|
454
498
|
const register = server.registerTool.bind(server);
|
|
455
499
|
register("myob_list_items", {
|
|
456
500
|
description: "List inventory items from a connected MYOB Business.",
|
|
501
|
+
annotations: readOnlyToolAnnotations,
|
|
457
502
|
inputSchema: {
|
|
458
503
|
myobBusinessId: myobBusinessIdField,
|
|
459
504
|
filter: z.string().optional().describe("OData $filter expression"),
|
|
@@ -474,12 +519,13 @@ function registerInventoryTools(server, resolveClient) {
|
|
|
474
519
|
});
|
|
475
520
|
register("myob_get_item", {
|
|
476
521
|
description: "Get a specific inventory item by UID from a connected MYOB Business.",
|
|
522
|
+
annotations: readOnlyToolAnnotations,
|
|
477
523
|
inputSchema: {
|
|
478
524
|
myobBusinessId: myobBusinessIdField,
|
|
479
|
-
uid:
|
|
525
|
+
uid: myobResourceUidField.describe("The item UID (GUID)")
|
|
480
526
|
}
|
|
481
527
|
}, async (args) => {
|
|
482
|
-
const result = await resolveClient(args.myobBusinessId).get(
|
|
528
|
+
const result = await resolveClient(args.myobBusinessId).get(myobResourcePath("/Inventory/Item", args.uid));
|
|
483
529
|
return { content: [{
|
|
484
530
|
type: "text",
|
|
485
531
|
text: JSON.stringify(result, null, 2)
|
|
@@ -487,6 +533,7 @@ function registerInventoryTools(server, resolveClient) {
|
|
|
487
533
|
});
|
|
488
534
|
register("myob_create_item", {
|
|
489
535
|
description: "Create a new inventory item in a connected MYOB Business.",
|
|
536
|
+
annotations: additiveToolAnnotations,
|
|
490
537
|
inputSchema: {
|
|
491
538
|
myobBusinessId: myobBusinessIdField,
|
|
492
539
|
data: z.record(z.string(), z.unknown()).describe("Full item data object including Name, Number, SellingDetails, BuyingDetails, etc.")
|
|
@@ -500,13 +547,14 @@ function registerInventoryTools(server, resolveClient) {
|
|
|
500
547
|
});
|
|
501
548
|
register("myob_update_item", {
|
|
502
549
|
description: "Update an existing inventory item in a connected MYOB Business.",
|
|
550
|
+
annotations: updateToolAnnotations,
|
|
503
551
|
inputSchema: {
|
|
504
552
|
myobBusinessId: myobBusinessIdField,
|
|
505
|
-
uid:
|
|
553
|
+
uid: myobResourceUidField.describe("The item UID (GUID) to update"),
|
|
506
554
|
data: z.record(z.string(), z.unknown()).describe("Item data fields to update")
|
|
507
555
|
}
|
|
508
556
|
}, async (args) => {
|
|
509
|
-
const result = await resolveClient(args.myobBusinessId).put(
|
|
557
|
+
const result = await resolveClient(args.myobBusinessId).put(myobResourcePath("/Inventory/Item", args.uid), args.data);
|
|
510
558
|
return { content: [{
|
|
511
559
|
type: "text",
|
|
512
560
|
text: JSON.stringify(result, null, 2)
|
|
@@ -519,6 +567,7 @@ function registerPaymentTools(server, resolveClient) {
|
|
|
519
567
|
const register = server.registerTool.bind(server);
|
|
520
568
|
register("myob_list_customer_payments", {
|
|
521
569
|
description: "List customer payments (receipts) from a connected MYOB Business.",
|
|
570
|
+
annotations: readOnlyToolAnnotations,
|
|
522
571
|
inputSchema: {
|
|
523
572
|
myobBusinessId: myobBusinessIdField,
|
|
524
573
|
filter: z.string().optional().describe("OData $filter expression"),
|
|
@@ -539,6 +588,7 @@ function registerPaymentTools(server, resolveClient) {
|
|
|
539
588
|
});
|
|
540
589
|
register("myob_record_customer_payment", {
|
|
541
590
|
description: "Record a customer payment against invoices in a connected MYOB Business.",
|
|
591
|
+
annotations: additiveToolAnnotations,
|
|
542
592
|
inputSchema: {
|
|
543
593
|
myobBusinessId: myobBusinessIdField,
|
|
544
594
|
data: z.record(z.string(), z.unknown()).describe("Full customer payment data including Customer, Account, Invoices, AmountReceived, etc.")
|
|
@@ -552,6 +602,7 @@ function registerPaymentTools(server, resolveClient) {
|
|
|
552
602
|
});
|
|
553
603
|
register("myob_list_supplier_payments", {
|
|
554
604
|
description: "List supplier payments from a connected MYOB Business.",
|
|
605
|
+
annotations: readOnlyToolAnnotations,
|
|
555
606
|
inputSchema: {
|
|
556
607
|
myobBusinessId: myobBusinessIdField,
|
|
557
608
|
filter: z.string().optional().describe("OData $filter expression"),
|
|
@@ -572,6 +623,7 @@ function registerPaymentTools(server, resolveClient) {
|
|
|
572
623
|
});
|
|
573
624
|
register("myob_record_supplier_payment", {
|
|
574
625
|
description: "Record a supplier payment against bills in a connected MYOB Business.",
|
|
626
|
+
annotations: additiveToolAnnotations,
|
|
575
627
|
inputSchema: {
|
|
576
628
|
myobBusinessId: myobBusinessIdField,
|
|
577
629
|
data: z.record(z.string(), z.unknown()).describe("Full supplier payment data including Supplier, Account, Bills, AmountPaid, etc.")
|
|
@@ -585,6 +637,39 @@ function registerPaymentTools(server, resolveClient) {
|
|
|
585
637
|
});
|
|
586
638
|
}
|
|
587
639
|
//#endregion
|
|
640
|
+
//#region src/token-refresh.ts
|
|
641
|
+
/**
|
|
642
|
+
* Coordinates MYOB refreshes without allowing credentials to cross business
|
|
643
|
+
* boundaries. Calls for the same Connection share one request; calls for
|
|
644
|
+
* different Connections remain independent.
|
|
645
|
+
*/
|
|
646
|
+
var MyobTokenRefreshCoordinator = class {
|
|
647
|
+
inFlight = /* @__PURE__ */ new Map();
|
|
648
|
+
constructor(apiClient, targets) {
|
|
649
|
+
this.apiClient = apiClient;
|
|
650
|
+
this.targets = targets;
|
|
651
|
+
}
|
|
652
|
+
refreshBusiness(myobBusinessId) {
|
|
653
|
+
const target = this.targets.get(myobBusinessId);
|
|
654
|
+
if (!target) return Promise.reject(/* @__PURE__ */ new Error(`Unknown myobBusinessId: ${myobBusinessId}`));
|
|
655
|
+
const existing = this.inFlight.get(target.accountIdentifier);
|
|
656
|
+
if (existing) return existing;
|
|
657
|
+
const refresh = this.apiClient.refreshMYOBAccountToken(target.accountIdentifier).then(({ accessToken }) => {
|
|
658
|
+
target.client.updateToken(accessToken);
|
|
659
|
+
return accessToken;
|
|
660
|
+
}).finally(() => {
|
|
661
|
+
if (this.inFlight.get(target.accountIdentifier) === refresh) this.inFlight.delete(target.accountIdentifier);
|
|
662
|
+
});
|
|
663
|
+
this.inFlight.set(target.accountIdentifier, refresh);
|
|
664
|
+
return refresh;
|
|
665
|
+
}
|
|
666
|
+
async refreshAll() {
|
|
667
|
+
const businessIds = [...this.targets.keys()];
|
|
668
|
+
await Promise.all(businessIds.map((businessId) => this.refreshBusiness(businessId)));
|
|
669
|
+
return businessIds;
|
|
670
|
+
}
|
|
671
|
+
};
|
|
672
|
+
//#endregion
|
|
588
673
|
//#region src/server.ts
|
|
589
674
|
/**
|
|
590
675
|
* MYOB Business MCP Server (Pattern A multi-account)
|
|
@@ -604,7 +689,6 @@ function registerPaymentTools(server, resolveClient) {
|
|
|
604
689
|
*/
|
|
605
690
|
const clients = /* @__PURE__ */ new Map();
|
|
606
691
|
let refreshTimer = null;
|
|
607
|
-
let inFlightRefresh = null;
|
|
608
692
|
const REFRESH_INTERVAL_MS = 900 * 1e3;
|
|
609
693
|
const REFRESH_RETRY_MS = 60 * 1e3;
|
|
610
694
|
function log(msg) {
|
|
@@ -638,55 +722,26 @@ function zodShapeToValidatable(name, shape) {
|
|
|
638
722
|
* the valid set. This is the per-call dispatch contract for Pattern A.
|
|
639
723
|
*/
|
|
640
724
|
function resolveMyobClient(myobBusinessId) {
|
|
641
|
-
const
|
|
642
|
-
if (!
|
|
643
|
-
return
|
|
644
|
-
}
|
|
645
|
-
/**
|
|
646
|
-
* Single fan-out point for token refresh. Used by:
|
|
647
|
-
* - the scheduled timer (performRefresh)
|
|
648
|
-
* - the `myob_refresh_token` tool
|
|
649
|
-
* - every cached `MyobClient`'s `onRefresh` callback (on 401 retry)
|
|
650
|
-
*
|
|
651
|
-
* Concurrent callers share one in-flight `refreshMYOBToken()` request
|
|
652
|
-
* via `inFlightRefresh`, and the resolved token is fanned out to every
|
|
653
|
-
* cached client so siblings don't re-hit the network on their own 401s.
|
|
654
|
-
*
|
|
655
|
-
* v1 limitation: `apiClient.refreshMYOBToken()` refreshes the primary
|
|
656
|
-
* connection's token only and we fan it across all clients. This works
|
|
657
|
-
* as long as MYOB access tokens are interchangeable across businesses
|
|
658
|
-
* sharing a partner appId (true today). Per-connection refresh is a v2
|
|
659
|
-
* follow-up — see DEVELOPING.md.
|
|
660
|
-
*/
|
|
661
|
-
async function refreshAndFanOut(apiClient) {
|
|
662
|
-
if (inFlightRefresh) return inFlightRefresh;
|
|
663
|
-
inFlightRefresh = (async () => {
|
|
664
|
-
try {
|
|
665
|
-
const { accessToken } = await apiClient.refreshMYOBToken();
|
|
666
|
-
for (const client of clients.values()) client.updateToken(accessToken);
|
|
667
|
-
return accessToken;
|
|
668
|
-
} finally {
|
|
669
|
-
inFlightRefresh = null;
|
|
670
|
-
}
|
|
671
|
-
})();
|
|
672
|
-
return inFlightRefresh;
|
|
725
|
+
const account = clients.get(myobBusinessId);
|
|
726
|
+
if (!account) throw new Error(`Unknown myobBusinessId: ${myobBusinessId}. Call myob_list_businesses to see the connected businesses on this agent.`);
|
|
727
|
+
return account.client;
|
|
673
728
|
}
|
|
674
|
-
async function performRefresh(
|
|
729
|
+
async function performRefresh(tokenRefresher) {
|
|
675
730
|
log("Refreshing MYOB access tokens across all connected businesses...");
|
|
676
|
-
await
|
|
677
|
-
log(
|
|
731
|
+
const refreshed = await tokenRefresher.refreshAll();
|
|
732
|
+
log(`Refreshed ${String(refreshed.length)} MYOB business token(s)`);
|
|
678
733
|
}
|
|
679
|
-
function scheduleRefresh(
|
|
734
|
+
function scheduleRefresh(tokenRefresher) {
|
|
680
735
|
if (refreshTimer) clearTimeout(refreshTimer);
|
|
681
736
|
refreshTimer = setTimeout(() => {
|
|
682
737
|
(async () => {
|
|
683
738
|
try {
|
|
684
|
-
await performRefresh(
|
|
685
|
-
scheduleRefresh(
|
|
739
|
+
await performRefresh(tokenRefresher);
|
|
740
|
+
scheduleRefresh(tokenRefresher);
|
|
686
741
|
} catch (err) {
|
|
687
742
|
log(`Token refresh failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
688
743
|
refreshTimer = setTimeout(() => {
|
|
689
|
-
scheduleRefresh(
|
|
744
|
+
scheduleRefresh(tokenRefresher);
|
|
690
745
|
}, REFRESH_RETRY_MS);
|
|
691
746
|
}
|
|
692
747
|
})();
|
|
@@ -700,19 +755,26 @@ async function main() {
|
|
|
700
755
|
});
|
|
701
756
|
const { accounts } = await apiClient.getMYOBAccounts();
|
|
702
757
|
if (accounts.length === 0) log("No MYOB businesses connected — server will start with myob_list_businesses + myob_refresh_token only");
|
|
758
|
+
const tokenRefresher = new MyobTokenRefreshCoordinator(apiClient, clients);
|
|
703
759
|
for (const acct of accounts) {
|
|
704
760
|
if (clients.has(acct.myobBusinessId)) {
|
|
705
761
|
log(`Duplicate myobBusinessId ${acct.myobBusinessId} returned by getMYOBAccounts() — keeping the first cached client`);
|
|
706
762
|
continue;
|
|
707
763
|
}
|
|
708
|
-
|
|
764
|
+
const client = new MyobClient({
|
|
709
765
|
accessToken: acct.accessToken,
|
|
710
766
|
clientId: acct.clientId,
|
|
711
767
|
businessId: acct.myobBusinessId,
|
|
712
768
|
onRefresh: async () => {
|
|
713
|
-
return { accessToken: await
|
|
769
|
+
return { accessToken: await tokenRefresher.refreshBusiness(acct.myobBusinessId) };
|
|
714
770
|
}
|
|
715
|
-
})
|
|
771
|
+
});
|
|
772
|
+
clients.set(acct.myobBusinessId, {
|
|
773
|
+
accountIdentifier: acct.accountIdentifier,
|
|
774
|
+
displayName: acct.displayName,
|
|
775
|
+
connectedAt: acct.connectedAt,
|
|
776
|
+
client
|
|
777
|
+
});
|
|
716
778
|
log(`Cached client for business ${acct.myobBusinessId} (${acct.displayName ?? "no display name"})`);
|
|
717
779
|
}
|
|
718
780
|
const server = new McpServer({
|
|
@@ -733,23 +795,32 @@ async function main() {
|
|
|
733
795
|
registerInventoryTools(server, resolveMyobClient);
|
|
734
796
|
registerPaymentTools(server, resolveMyobClient);
|
|
735
797
|
const registerTool = server.registerTool.bind(server);
|
|
736
|
-
registerTool("myob_list_businesses", {
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
798
|
+
registerTool("myob_list_businesses", {
|
|
799
|
+
description: "List the MYOB Business accounts (companies) the agent has connected. Returns one entry per OAuth connection — use the returned myobBusinessId values as the `myobBusinessId` selector arg on every other myob_* tool.",
|
|
800
|
+
annotations: readOnlyToolAnnotations
|
|
801
|
+
}, () => {
|
|
802
|
+
const summaries = [...clients.entries()].map(([myobBusinessId, account]) => ({
|
|
803
|
+
myobBusinessId,
|
|
804
|
+
displayName: account.displayName,
|
|
805
|
+
connectedAt: account.connectedAt
|
|
741
806
|
}));
|
|
742
807
|
return { content: [{
|
|
743
808
|
type: "text",
|
|
744
809
|
text: JSON.stringify({ businesses: summaries }, null, 2)
|
|
745
810
|
}] };
|
|
746
811
|
});
|
|
747
|
-
registerTool("myob_refresh_token", {
|
|
812
|
+
registerTool("myob_refresh_token", {
|
|
813
|
+
description: "Force-refresh every connected MYOB business. Each Connection is refreshed independently so credentials never cross business boundaries. Use this if API calls are failing with authentication errors.",
|
|
814
|
+
annotations: additiveToolAnnotations
|
|
815
|
+
}, async () => {
|
|
748
816
|
try {
|
|
749
|
-
await
|
|
817
|
+
const refreshed = await tokenRefresher.refreshAll();
|
|
750
818
|
return { content: [{
|
|
751
819
|
type: "text",
|
|
752
|
-
text: JSON.stringify({
|
|
820
|
+
text: JSON.stringify({
|
|
821
|
+
refreshed: true,
|
|
822
|
+
businesses: refreshed
|
|
823
|
+
})
|
|
753
824
|
}] };
|
|
754
825
|
} catch (err) {
|
|
755
826
|
return {
|
|
@@ -765,7 +836,7 @@ async function main() {
|
|
|
765
836
|
selector: "myobBusinessId",
|
|
766
837
|
exempt: ["myob_list_businesses", "myob_refresh_token"]
|
|
767
838
|
});
|
|
768
|
-
scheduleRefresh(
|
|
839
|
+
scheduleRefresh(tokenRefresher);
|
|
769
840
|
const transport = new StdioServerTransport();
|
|
770
841
|
await server.connect(transport);
|
|
771
842
|
log(`MYOB MCP server running with ${String(accounts.length)} connected business(es) and Pattern A selector enforcement`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alfe.ai/myob-mcp",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.18",
|
|
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",
|
|
@@ -19,9 +19,9 @@
|
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
21
21
|
"zod": "^4.0.5",
|
|
22
|
-
"@alfe.ai/agent-api-client": "0.
|
|
23
|
-
"@alfe.ai/config": "0.4.
|
|
24
|
-
"@alfe.ai/mcp-bundler": "0.4.
|
|
22
|
+
"@alfe.ai/agent-api-client": "0.16.0",
|
|
23
|
+
"@alfe.ai/config": "0.4.1",
|
|
24
|
+
"@alfe.ai/mcp-bundler": "0.4.1"
|
|
25
25
|
},
|
|
26
26
|
"license": "UNLICENSED",
|
|
27
27
|
"homepage": "https://alfe.ai",
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
"scripts": {
|
|
38
38
|
"build": "tsdown",
|
|
39
39
|
"dev": "tsdown --watch",
|
|
40
|
+
"test": "vitest run",
|
|
40
41
|
"typecheck": "tsc --noEmit",
|
|
41
42
|
"lint": "eslint ."
|
|
42
43
|
}
|