@alfe.ai/myob-mcp 0.3.15 → 0.3.17
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 +177 -72
- package/package.json +5 -3
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
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { z } from "zod";
|
|
4
5
|
import { resolveConfig } from "@alfe.ai/config";
|
|
5
6
|
import { AgentApiClient } from "@alfe.ai/agent-api-client";
|
|
6
|
-
import {
|
|
7
|
+
import { assertPatternA } from "@alfe.ai/mcp-bundler";
|
|
7
8
|
//#region src/myob-client.ts
|
|
8
9
|
/**
|
|
9
10
|
* MYOB Business API client — wraps fetch with required headers,
|
|
@@ -15,6 +16,7 @@ import { z } from "zod";
|
|
|
15
16
|
* - x-myobapi-version: v2
|
|
16
17
|
*/
|
|
17
18
|
const MYOB_API_BASE = "https://api.myob.com/accountright";
|
|
19
|
+
const MYOB_REQUEST_TIMEOUT_MS = 3e4;
|
|
18
20
|
var MyobClient = class {
|
|
19
21
|
accessToken;
|
|
20
22
|
clientId;
|
|
@@ -30,7 +32,7 @@ var MyobClient = class {
|
|
|
30
32
|
this.accessToken = accessToken;
|
|
31
33
|
}
|
|
32
34
|
get baseUrl() {
|
|
33
|
-
return `${MYOB_API_BASE}/${this.businessId}`;
|
|
35
|
+
return `${MYOB_API_BASE}/${encodeURIComponent(this.businessId)}`;
|
|
34
36
|
}
|
|
35
37
|
headers() {
|
|
36
38
|
return {
|
|
@@ -62,7 +64,8 @@ var MyobClient = class {
|
|
|
62
64
|
const doFetch = async () => {
|
|
63
65
|
const init = {
|
|
64
66
|
method,
|
|
65
|
-
headers: this.headers()
|
|
67
|
+
headers: this.headers(),
|
|
68
|
+
signal: AbortSignal.timeout(MYOB_REQUEST_TIMEOUT_MS)
|
|
66
69
|
};
|
|
67
70
|
if (body !== void 0) init.body = JSON.stringify(body);
|
|
68
71
|
return fetch(url, init);
|
|
@@ -89,13 +92,36 @@ var MyobClient = class {
|
|
|
89
92
|
* expects a raw Zod shape (`{ field: z.string() }`), NOT a JSON-Schema
|
|
90
93
|
* object — passing JSON-Schema throws synchronously at registration time.
|
|
91
94
|
*/
|
|
92
|
-
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
|
+
};
|
|
93
118
|
//#endregion
|
|
94
119
|
//#region src/tools/contacts.ts
|
|
95
120
|
function registerContactTools(server, resolveClient) {
|
|
96
121
|
const register = server.registerTool.bind(server);
|
|
97
122
|
register("myob_list_customers", {
|
|
98
123
|
description: "List customer contacts from a connected MYOB Business.",
|
|
124
|
+
annotations: readOnlyToolAnnotations,
|
|
99
125
|
inputSchema: {
|
|
100
126
|
myobBusinessId: myobBusinessIdField,
|
|
101
127
|
filter: z.string().optional().describe("OData $filter expression (e.g. \"IsActive eq true\")"),
|
|
@@ -116,12 +142,13 @@ function registerContactTools(server, resolveClient) {
|
|
|
116
142
|
});
|
|
117
143
|
register("myob_get_customer", {
|
|
118
144
|
description: "Get a specific customer contact by UID from a connected MYOB Business.",
|
|
145
|
+
annotations: readOnlyToolAnnotations,
|
|
119
146
|
inputSchema: {
|
|
120
147
|
myobBusinessId: myobBusinessIdField,
|
|
121
|
-
uid:
|
|
148
|
+
uid: myobResourceUidField.describe("The customer UID (GUID)")
|
|
122
149
|
}
|
|
123
150
|
}, async (args) => {
|
|
124
|
-
const result = await resolveClient(args.myobBusinessId).get(
|
|
151
|
+
const result = await resolveClient(args.myobBusinessId).get(myobResourcePath("/Contact/Customer", args.uid));
|
|
125
152
|
return { content: [{
|
|
126
153
|
type: "text",
|
|
127
154
|
text: JSON.stringify(result, null, 2)
|
|
@@ -129,6 +156,7 @@ function registerContactTools(server, resolveClient) {
|
|
|
129
156
|
});
|
|
130
157
|
register("myob_create_customer", {
|
|
131
158
|
description: "Create a new customer contact in a connected MYOB Business.",
|
|
159
|
+
annotations: additiveToolAnnotations,
|
|
132
160
|
inputSchema: {
|
|
133
161
|
myobBusinessId: myobBusinessIdField,
|
|
134
162
|
CompanyName: z.string().optional().describe("Company name"),
|
|
@@ -154,13 +182,14 @@ function registerContactTools(server, resolveClient) {
|
|
|
154
182
|
});
|
|
155
183
|
register("myob_update_customer", {
|
|
156
184
|
description: "Update an existing customer contact in a connected MYOB Business.",
|
|
185
|
+
annotations: updateToolAnnotations,
|
|
157
186
|
inputSchema: {
|
|
158
187
|
myobBusinessId: myobBusinessIdField,
|
|
159
|
-
uid:
|
|
188
|
+
uid: myobResourceUidField.describe("The customer UID (GUID) to update"),
|
|
160
189
|
data: z.record(z.string(), z.unknown()).describe("Customer data fields to update")
|
|
161
190
|
}
|
|
162
191
|
}, async (args) => {
|
|
163
|
-
const result = await resolveClient(args.myobBusinessId).put(
|
|
192
|
+
const result = await resolveClient(args.myobBusinessId).put(myobResourcePath("/Contact/Customer", args.uid), args.data);
|
|
164
193
|
return { content: [{
|
|
165
194
|
type: "text",
|
|
166
195
|
text: JSON.stringify(result, null, 2)
|
|
@@ -168,6 +197,7 @@ function registerContactTools(server, resolveClient) {
|
|
|
168
197
|
});
|
|
169
198
|
register("myob_list_suppliers", {
|
|
170
199
|
description: "List supplier contacts from a connected MYOB Business.",
|
|
200
|
+
annotations: readOnlyToolAnnotations,
|
|
171
201
|
inputSchema: {
|
|
172
202
|
myobBusinessId: myobBusinessIdField,
|
|
173
203
|
filter: z.string().optional().describe("OData $filter expression"),
|
|
@@ -188,12 +218,13 @@ function registerContactTools(server, resolveClient) {
|
|
|
188
218
|
});
|
|
189
219
|
register("myob_get_supplier", {
|
|
190
220
|
description: "Get a specific supplier contact by UID from a connected MYOB Business.",
|
|
221
|
+
annotations: readOnlyToolAnnotations,
|
|
191
222
|
inputSchema: {
|
|
192
223
|
myobBusinessId: myobBusinessIdField,
|
|
193
|
-
uid:
|
|
224
|
+
uid: myobResourceUidField.describe("The supplier UID (GUID)")
|
|
194
225
|
}
|
|
195
226
|
}, async (args) => {
|
|
196
|
-
const result = await resolveClient(args.myobBusinessId).get(
|
|
227
|
+
const result = await resolveClient(args.myobBusinessId).get(myobResourcePath("/Contact/Supplier", args.uid));
|
|
197
228
|
return { content: [{
|
|
198
229
|
type: "text",
|
|
199
230
|
text: JSON.stringify(result, null, 2)
|
|
@@ -201,6 +232,7 @@ function registerContactTools(server, resolveClient) {
|
|
|
201
232
|
});
|
|
202
233
|
register("myob_create_supplier", {
|
|
203
234
|
description: "Create a new supplier contact in a connected MYOB Business.",
|
|
235
|
+
annotations: additiveToolAnnotations,
|
|
204
236
|
inputSchema: {
|
|
205
237
|
myobBusinessId: myobBusinessIdField,
|
|
206
238
|
CompanyName: z.string().optional().describe("Company name"),
|
|
@@ -231,6 +263,7 @@ function registerSalesTools(server, resolveClient) {
|
|
|
231
263
|
const register = server.registerTool.bind(server);
|
|
232
264
|
register("myob_list_invoices", {
|
|
233
265
|
description: "List sale invoices from a connected MYOB Business.",
|
|
266
|
+
annotations: readOnlyToolAnnotations,
|
|
234
267
|
inputSchema: {
|
|
235
268
|
myobBusinessId: myobBusinessIdField,
|
|
236
269
|
filter: z.string().optional().describe("OData $filter expression (e.g. \"Status eq 'Open'\")"),
|
|
@@ -251,12 +284,13 @@ function registerSalesTools(server, resolveClient) {
|
|
|
251
284
|
});
|
|
252
285
|
register("myob_get_invoice", {
|
|
253
286
|
description: "Get a specific sale invoice by UID from a connected MYOB Business.",
|
|
287
|
+
annotations: readOnlyToolAnnotations,
|
|
254
288
|
inputSchema: {
|
|
255
289
|
myobBusinessId: myobBusinessIdField,
|
|
256
|
-
uid:
|
|
290
|
+
uid: myobResourceUidField.describe("The invoice UID (GUID)")
|
|
257
291
|
}
|
|
258
292
|
}, async (args) => {
|
|
259
|
-
const result = await resolveClient(args.myobBusinessId).get(
|
|
293
|
+
const result = await resolveClient(args.myobBusinessId).get(myobResourcePath("/Sale/Invoice", args.uid));
|
|
260
294
|
return { content: [{
|
|
261
295
|
type: "text",
|
|
262
296
|
text: JSON.stringify(result, null, 2)
|
|
@@ -264,6 +298,7 @@ function registerSalesTools(server, resolveClient) {
|
|
|
264
298
|
});
|
|
265
299
|
register("myob_create_invoice", {
|
|
266
300
|
description: "Create a new sale invoice in a connected MYOB Business. Provide customer, line items, and dates.",
|
|
301
|
+
annotations: additiveToolAnnotations,
|
|
267
302
|
inputSchema: {
|
|
268
303
|
myobBusinessId: myobBusinessIdField,
|
|
269
304
|
data: z.record(z.string(), z.unknown()).describe("Full invoice data object including Customer, Date, Lines, etc.")
|
|
@@ -277,6 +312,7 @@ function registerSalesTools(server, resolveClient) {
|
|
|
277
312
|
});
|
|
278
313
|
register("myob_list_quotes", {
|
|
279
314
|
description: "List sale quotes from a connected MYOB Business.",
|
|
315
|
+
annotations: readOnlyToolAnnotations,
|
|
280
316
|
inputSchema: {
|
|
281
317
|
myobBusinessId: myobBusinessIdField,
|
|
282
318
|
filter: z.string().optional().describe("OData $filter expression"),
|
|
@@ -297,6 +333,7 @@ function registerSalesTools(server, resolveClient) {
|
|
|
297
333
|
});
|
|
298
334
|
register("myob_create_quote", {
|
|
299
335
|
description: "Create a new sale quote in a connected MYOB Business.",
|
|
336
|
+
annotations: additiveToolAnnotations,
|
|
300
337
|
inputSchema: {
|
|
301
338
|
myobBusinessId: myobBusinessIdField,
|
|
302
339
|
data: z.record(z.string(), z.unknown()).describe("Full quote data object including Customer, Date, Lines, etc.")
|
|
@@ -315,6 +352,7 @@ function registerPurchaseTools(server, resolveClient) {
|
|
|
315
352
|
const register = server.registerTool.bind(server);
|
|
316
353
|
register("myob_list_bills", {
|
|
317
354
|
description: "List purchase bills from a connected MYOB Business.",
|
|
355
|
+
annotations: readOnlyToolAnnotations,
|
|
318
356
|
inputSchema: {
|
|
319
357
|
myobBusinessId: myobBusinessIdField,
|
|
320
358
|
filter: z.string().optional().describe("OData $filter expression"),
|
|
@@ -335,12 +373,13 @@ function registerPurchaseTools(server, resolveClient) {
|
|
|
335
373
|
});
|
|
336
374
|
register("myob_get_bill", {
|
|
337
375
|
description: "Get a specific purchase bill by UID from a connected MYOB Business.",
|
|
376
|
+
annotations: readOnlyToolAnnotations,
|
|
338
377
|
inputSchema: {
|
|
339
378
|
myobBusinessId: myobBusinessIdField,
|
|
340
|
-
uid:
|
|
379
|
+
uid: myobResourceUidField.describe("The bill UID (GUID)")
|
|
341
380
|
}
|
|
342
381
|
}, async (args) => {
|
|
343
|
-
const result = await resolveClient(args.myobBusinessId).get(
|
|
382
|
+
const result = await resolveClient(args.myobBusinessId).get(myobResourcePath("/Purchase/Bill", args.uid));
|
|
344
383
|
return { content: [{
|
|
345
384
|
type: "text",
|
|
346
385
|
text: JSON.stringify(result, null, 2)
|
|
@@ -348,6 +387,7 @@ function registerPurchaseTools(server, resolveClient) {
|
|
|
348
387
|
});
|
|
349
388
|
register("myob_create_bill", {
|
|
350
389
|
description: "Create a new purchase bill in a connected MYOB Business.",
|
|
390
|
+
annotations: additiveToolAnnotations,
|
|
351
391
|
inputSchema: {
|
|
352
392
|
myobBusinessId: myobBusinessIdField,
|
|
353
393
|
data: z.record(z.string(), z.unknown()).describe("Full bill data object including Supplier, Date, Lines, etc.")
|
|
@@ -366,6 +406,7 @@ function registerGeneralLedgerTools(server, resolveClient) {
|
|
|
366
406
|
const register = server.registerTool.bind(server);
|
|
367
407
|
register("myob_list_accounts", {
|
|
368
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,
|
|
369
410
|
inputSchema: {
|
|
370
411
|
myobBusinessId: myobBusinessIdField,
|
|
371
412
|
filter: z.string().optional().describe("OData $filter expression (e.g. \"Type eq 'Bank'\")"),
|
|
@@ -386,12 +427,13 @@ function registerGeneralLedgerTools(server, resolveClient) {
|
|
|
386
427
|
});
|
|
387
428
|
register("myob_get_account", {
|
|
388
429
|
description: "Get a specific general ledger account by UID from a connected MYOB Business.",
|
|
430
|
+
annotations: readOnlyToolAnnotations,
|
|
389
431
|
inputSchema: {
|
|
390
432
|
myobBusinessId: myobBusinessIdField,
|
|
391
|
-
uid:
|
|
433
|
+
uid: myobResourceUidField.describe("The account UID (GUID)")
|
|
392
434
|
}
|
|
393
435
|
}, async (args) => {
|
|
394
|
-
const result = await resolveClient(args.myobBusinessId).get(
|
|
436
|
+
const result = await resolveClient(args.myobBusinessId).get(myobResourcePath("/GeneralLedger/Account", args.uid));
|
|
395
437
|
return { content: [{
|
|
396
438
|
type: "text",
|
|
397
439
|
text: JSON.stringify(result, null, 2)
|
|
@@ -399,6 +441,7 @@ function registerGeneralLedgerTools(server, resolveClient) {
|
|
|
399
441
|
});
|
|
400
442
|
register("myob_list_journal_entries", {
|
|
401
443
|
description: "List journal transactions from a connected MYOB Business general ledger.",
|
|
444
|
+
annotations: readOnlyToolAnnotations,
|
|
402
445
|
inputSchema: {
|
|
403
446
|
myobBusinessId: myobBusinessIdField,
|
|
404
447
|
filter: z.string().optional().describe("OData $filter expression"),
|
|
@@ -419,6 +462,7 @@ function registerGeneralLedgerTools(server, resolveClient) {
|
|
|
419
462
|
});
|
|
420
463
|
register("myob_create_journal_entry", {
|
|
421
464
|
description: "Create a new journal transaction in a connected MYOB Business.",
|
|
465
|
+
annotations: additiveToolAnnotations,
|
|
422
466
|
inputSchema: {
|
|
423
467
|
myobBusinessId: myobBusinessIdField,
|
|
424
468
|
data: z.record(z.string(), z.unknown()).describe("Full journal transaction data object including Lines, DateOccurred, Memo, etc.")
|
|
@@ -432,6 +476,7 @@ function registerGeneralLedgerTools(server, resolveClient) {
|
|
|
432
476
|
});
|
|
433
477
|
register("myob_list_tax_codes", {
|
|
434
478
|
description: "List tax codes from a connected MYOB Business (read-only for MYOB Essentials).",
|
|
479
|
+
annotations: readOnlyToolAnnotations,
|
|
435
480
|
inputSchema: {
|
|
436
481
|
myobBusinessId: myobBusinessIdField,
|
|
437
482
|
filter: z.string().optional().describe("OData $filter expression")
|
|
@@ -453,6 +498,7 @@ function registerInventoryTools(server, resolveClient) {
|
|
|
453
498
|
const register = server.registerTool.bind(server);
|
|
454
499
|
register("myob_list_items", {
|
|
455
500
|
description: "List inventory items from a connected MYOB Business.",
|
|
501
|
+
annotations: readOnlyToolAnnotations,
|
|
456
502
|
inputSchema: {
|
|
457
503
|
myobBusinessId: myobBusinessIdField,
|
|
458
504
|
filter: z.string().optional().describe("OData $filter expression"),
|
|
@@ -473,12 +519,13 @@ function registerInventoryTools(server, resolveClient) {
|
|
|
473
519
|
});
|
|
474
520
|
register("myob_get_item", {
|
|
475
521
|
description: "Get a specific inventory item by UID from a connected MYOB Business.",
|
|
522
|
+
annotations: readOnlyToolAnnotations,
|
|
476
523
|
inputSchema: {
|
|
477
524
|
myobBusinessId: myobBusinessIdField,
|
|
478
|
-
uid:
|
|
525
|
+
uid: myobResourceUidField.describe("The item UID (GUID)")
|
|
479
526
|
}
|
|
480
527
|
}, async (args) => {
|
|
481
|
-
const result = await resolveClient(args.myobBusinessId).get(
|
|
528
|
+
const result = await resolveClient(args.myobBusinessId).get(myobResourcePath("/Inventory/Item", args.uid));
|
|
482
529
|
return { content: [{
|
|
483
530
|
type: "text",
|
|
484
531
|
text: JSON.stringify(result, null, 2)
|
|
@@ -486,6 +533,7 @@ function registerInventoryTools(server, resolveClient) {
|
|
|
486
533
|
});
|
|
487
534
|
register("myob_create_item", {
|
|
488
535
|
description: "Create a new inventory item in a connected MYOB Business.",
|
|
536
|
+
annotations: additiveToolAnnotations,
|
|
489
537
|
inputSchema: {
|
|
490
538
|
myobBusinessId: myobBusinessIdField,
|
|
491
539
|
data: z.record(z.string(), z.unknown()).describe("Full item data object including Name, Number, SellingDetails, BuyingDetails, etc.")
|
|
@@ -499,13 +547,14 @@ function registerInventoryTools(server, resolveClient) {
|
|
|
499
547
|
});
|
|
500
548
|
register("myob_update_item", {
|
|
501
549
|
description: "Update an existing inventory item in a connected MYOB Business.",
|
|
550
|
+
annotations: updateToolAnnotations,
|
|
502
551
|
inputSchema: {
|
|
503
552
|
myobBusinessId: myobBusinessIdField,
|
|
504
|
-
uid:
|
|
553
|
+
uid: myobResourceUidField.describe("The item UID (GUID) to update"),
|
|
505
554
|
data: z.record(z.string(), z.unknown()).describe("Item data fields to update")
|
|
506
555
|
}
|
|
507
556
|
}, async (args) => {
|
|
508
|
-
const result = await resolveClient(args.myobBusinessId).put(
|
|
557
|
+
const result = await resolveClient(args.myobBusinessId).put(myobResourcePath("/Inventory/Item", args.uid), args.data);
|
|
509
558
|
return { content: [{
|
|
510
559
|
type: "text",
|
|
511
560
|
text: JSON.stringify(result, null, 2)
|
|
@@ -518,6 +567,7 @@ function registerPaymentTools(server, resolveClient) {
|
|
|
518
567
|
const register = server.registerTool.bind(server);
|
|
519
568
|
register("myob_list_customer_payments", {
|
|
520
569
|
description: "List customer payments (receipts) from a connected MYOB Business.",
|
|
570
|
+
annotations: readOnlyToolAnnotations,
|
|
521
571
|
inputSchema: {
|
|
522
572
|
myobBusinessId: myobBusinessIdField,
|
|
523
573
|
filter: z.string().optional().describe("OData $filter expression"),
|
|
@@ -538,6 +588,7 @@ function registerPaymentTools(server, resolveClient) {
|
|
|
538
588
|
});
|
|
539
589
|
register("myob_record_customer_payment", {
|
|
540
590
|
description: "Record a customer payment against invoices in a connected MYOB Business.",
|
|
591
|
+
annotations: additiveToolAnnotations,
|
|
541
592
|
inputSchema: {
|
|
542
593
|
myobBusinessId: myobBusinessIdField,
|
|
543
594
|
data: z.record(z.string(), z.unknown()).describe("Full customer payment data including Customer, Account, Invoices, AmountReceived, etc.")
|
|
@@ -551,6 +602,7 @@ function registerPaymentTools(server, resolveClient) {
|
|
|
551
602
|
});
|
|
552
603
|
register("myob_list_supplier_payments", {
|
|
553
604
|
description: "List supplier payments from a connected MYOB Business.",
|
|
605
|
+
annotations: readOnlyToolAnnotations,
|
|
554
606
|
inputSchema: {
|
|
555
607
|
myobBusinessId: myobBusinessIdField,
|
|
556
608
|
filter: z.string().optional().describe("OData $filter expression"),
|
|
@@ -571,6 +623,7 @@ function registerPaymentTools(server, resolveClient) {
|
|
|
571
623
|
});
|
|
572
624
|
register("myob_record_supplier_payment", {
|
|
573
625
|
description: "Record a supplier payment against bills in a connected MYOB Business.",
|
|
626
|
+
annotations: additiveToolAnnotations,
|
|
574
627
|
inputSchema: {
|
|
575
628
|
myobBusinessId: myobBusinessIdField,
|
|
576
629
|
data: z.record(z.string(), z.unknown()).describe("Full supplier payment data including Supplier, Account, Bills, AmountPaid, etc.")
|
|
@@ -584,6 +637,39 @@ function registerPaymentTools(server, resolveClient) {
|
|
|
584
637
|
});
|
|
585
638
|
}
|
|
586
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
|
|
587
673
|
//#region src/server.ts
|
|
588
674
|
/**
|
|
589
675
|
* MYOB Business MCP Server (Pattern A multi-account)
|
|
@@ -603,67 +689,59 @@ function registerPaymentTools(server, resolveClient) {
|
|
|
603
689
|
*/
|
|
604
690
|
const clients = /* @__PURE__ */ new Map();
|
|
605
691
|
let refreshTimer = null;
|
|
606
|
-
let inFlightRefresh = null;
|
|
607
692
|
const REFRESH_INTERVAL_MS = 900 * 1e3;
|
|
608
693
|
const REFRESH_RETRY_MS = 60 * 1e3;
|
|
609
694
|
function log(msg) {
|
|
610
695
|
process.stderr.write(`[myob-mcp-server] ${msg}\n`);
|
|
611
696
|
}
|
|
612
697
|
/**
|
|
698
|
+
* Convert a McpServer.registerTool Zod raw shape into the JSON-Schema-ish
|
|
699
|
+
* {@link ValidatableTool} the Pattern A validator inspects. Direct-MCP
|
|
700
|
+
* servers register Zod shapes (not JSON Schema), so the guard maps each
|
|
701
|
+
* property's presence / optionality / string-ness across.
|
|
702
|
+
*/
|
|
703
|
+
function zodShapeToValidatable(name, shape) {
|
|
704
|
+
const properties = {};
|
|
705
|
+
const required = [];
|
|
706
|
+
for (const [key, schema] of Object.entries(shape ?? {})) {
|
|
707
|
+
properties[key] = { type: schema instanceof z.ZodString ? "string" : "non-string" };
|
|
708
|
+
if (!schema.safeParse(void 0).success) required.push(key);
|
|
709
|
+
}
|
|
710
|
+
return {
|
|
711
|
+
name,
|
|
712
|
+
parameters: {
|
|
713
|
+
type: "object",
|
|
714
|
+
properties,
|
|
715
|
+
required
|
|
716
|
+
}
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
/**
|
|
613
720
|
* Resolve a `myobBusinessId` selector to its `MyobClient`. Throws if
|
|
614
721
|
* unknown — the LLM should call `myob_list_businesses` to discover
|
|
615
722
|
* the valid set. This is the per-call dispatch contract for Pattern A.
|
|
616
723
|
*/
|
|
617
724
|
function resolveMyobClient(myobBusinessId) {
|
|
618
|
-
const
|
|
619
|
-
if (!
|
|
620
|
-
return
|
|
621
|
-
}
|
|
622
|
-
/**
|
|
623
|
-
* Single fan-out point for token refresh. Used by:
|
|
624
|
-
* - the scheduled timer (performRefresh)
|
|
625
|
-
* - the `myob_refresh_token` tool
|
|
626
|
-
* - every cached `MyobClient`'s `onRefresh` callback (on 401 retry)
|
|
627
|
-
*
|
|
628
|
-
* Concurrent callers share one in-flight `refreshMYOBToken()` request
|
|
629
|
-
* via `inFlightRefresh`, and the resolved token is fanned out to every
|
|
630
|
-
* cached client so siblings don't re-hit the network on their own 401s.
|
|
631
|
-
*
|
|
632
|
-
* v1 limitation: `apiClient.refreshMYOBToken()` refreshes the primary
|
|
633
|
-
* connection's token only and we fan it across all clients. This works
|
|
634
|
-
* as long as MYOB access tokens are interchangeable across businesses
|
|
635
|
-
* sharing a partner appId (true today). Per-connection refresh is a v2
|
|
636
|
-
* follow-up — see DEVELOPING.md.
|
|
637
|
-
*/
|
|
638
|
-
async function refreshAndFanOut(apiClient) {
|
|
639
|
-
if (inFlightRefresh) return inFlightRefresh;
|
|
640
|
-
inFlightRefresh = (async () => {
|
|
641
|
-
try {
|
|
642
|
-
const { accessToken } = await apiClient.refreshMYOBToken();
|
|
643
|
-
for (const client of clients.values()) client.updateToken(accessToken);
|
|
644
|
-
return accessToken;
|
|
645
|
-
} finally {
|
|
646
|
-
inFlightRefresh = null;
|
|
647
|
-
}
|
|
648
|
-
})();
|
|
649
|
-
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;
|
|
650
728
|
}
|
|
651
|
-
async function performRefresh(
|
|
729
|
+
async function performRefresh(tokenRefresher) {
|
|
652
730
|
log("Refreshing MYOB access tokens across all connected businesses...");
|
|
653
|
-
await
|
|
654
|
-
log(
|
|
731
|
+
const refreshed = await tokenRefresher.refreshAll();
|
|
732
|
+
log(`Refreshed ${String(refreshed.length)} MYOB business token(s)`);
|
|
655
733
|
}
|
|
656
|
-
function scheduleRefresh(
|
|
734
|
+
function scheduleRefresh(tokenRefresher) {
|
|
657
735
|
if (refreshTimer) clearTimeout(refreshTimer);
|
|
658
736
|
refreshTimer = setTimeout(() => {
|
|
659
737
|
(async () => {
|
|
660
738
|
try {
|
|
661
|
-
await performRefresh(
|
|
662
|
-
scheduleRefresh(
|
|
739
|
+
await performRefresh(tokenRefresher);
|
|
740
|
+
scheduleRefresh(tokenRefresher);
|
|
663
741
|
} catch (err) {
|
|
664
742
|
log(`Token refresh failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
665
743
|
refreshTimer = setTimeout(() => {
|
|
666
|
-
scheduleRefresh(
|
|
744
|
+
scheduleRefresh(tokenRefresher);
|
|
667
745
|
}, REFRESH_RETRY_MS);
|
|
668
746
|
}
|
|
669
747
|
})();
|
|
@@ -677,25 +755,39 @@ async function main() {
|
|
|
677
755
|
});
|
|
678
756
|
const { accounts } = await apiClient.getMYOBAccounts();
|
|
679
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);
|
|
680
759
|
for (const acct of accounts) {
|
|
681
760
|
if (clients.has(acct.myobBusinessId)) {
|
|
682
761
|
log(`Duplicate myobBusinessId ${acct.myobBusinessId} returned by getMYOBAccounts() — keeping the first cached client`);
|
|
683
762
|
continue;
|
|
684
763
|
}
|
|
685
|
-
|
|
764
|
+
const client = new MyobClient({
|
|
686
765
|
accessToken: acct.accessToken,
|
|
687
766
|
clientId: acct.clientId,
|
|
688
767
|
businessId: acct.myobBusinessId,
|
|
689
768
|
onRefresh: async () => {
|
|
690
|
-
return { accessToken: await
|
|
769
|
+
return { accessToken: await tokenRefresher.refreshBusiness(acct.myobBusinessId) };
|
|
691
770
|
}
|
|
692
|
-
})
|
|
771
|
+
});
|
|
772
|
+
clients.set(acct.myobBusinessId, {
|
|
773
|
+
accountIdentifier: acct.accountIdentifier,
|
|
774
|
+
displayName: acct.displayName,
|
|
775
|
+
connectedAt: acct.connectedAt,
|
|
776
|
+
client
|
|
777
|
+
});
|
|
693
778
|
log(`Cached client for business ${acct.myobBusinessId} (${acct.displayName ?? "no display name"})`);
|
|
694
779
|
}
|
|
695
780
|
const server = new McpServer({
|
|
696
781
|
name: "myob-mcp-server",
|
|
697
782
|
version: "2.0.0"
|
|
698
783
|
});
|
|
784
|
+
const validatableTools = [];
|
|
785
|
+
const originalRegisterTool = server.registerTool.bind(server);
|
|
786
|
+
server.registerTool = (...args) => {
|
|
787
|
+
const [name, config] = args;
|
|
788
|
+
validatableTools.push(zodShapeToValidatable(name, config?.inputSchema));
|
|
789
|
+
return originalRegisterTool(...args);
|
|
790
|
+
};
|
|
699
791
|
registerContactTools(server, resolveMyobClient);
|
|
700
792
|
registerSalesTools(server, resolveMyobClient);
|
|
701
793
|
registerPurchaseTools(server, resolveMyobClient);
|
|
@@ -703,23 +795,32 @@ async function main() {
|
|
|
703
795
|
registerInventoryTools(server, resolveMyobClient);
|
|
704
796
|
registerPaymentTools(server, resolveMyobClient);
|
|
705
797
|
const registerTool = server.registerTool.bind(server);
|
|
706
|
-
registerTool("myob_list_businesses", {
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
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
|
|
711
806
|
}));
|
|
712
807
|
return { content: [{
|
|
713
808
|
type: "text",
|
|
714
809
|
text: JSON.stringify({ businesses: summaries }, null, 2)
|
|
715
810
|
}] };
|
|
716
811
|
});
|
|
717
|
-
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 () => {
|
|
718
816
|
try {
|
|
719
|
-
await
|
|
817
|
+
const refreshed = await tokenRefresher.refreshAll();
|
|
720
818
|
return { content: [{
|
|
721
819
|
type: "text",
|
|
722
|
-
text: JSON.stringify({
|
|
820
|
+
text: JSON.stringify({
|
|
821
|
+
refreshed: true,
|
|
822
|
+
businesses: refreshed
|
|
823
|
+
})
|
|
723
824
|
}] };
|
|
724
825
|
} catch (err) {
|
|
725
826
|
return {
|
|
@@ -731,7 +832,11 @@ async function main() {
|
|
|
731
832
|
};
|
|
732
833
|
}
|
|
733
834
|
});
|
|
734
|
-
|
|
835
|
+
assertPatternA(validatableTools, {
|
|
836
|
+
selector: "myobBusinessId",
|
|
837
|
+
exempt: ["myob_list_businesses", "myob_refresh_token"]
|
|
838
|
+
});
|
|
839
|
+
scheduleRefresh(tokenRefresher);
|
|
735
840
|
const transport = new StdioServerTransport();
|
|
736
841
|
await server.connect(transport);
|
|
737
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.17",
|
|
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,8 +19,9 @@
|
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
21
21
|
"zod": "^4.0.5",
|
|
22
|
-
"@alfe.ai/
|
|
23
|
-
"@alfe.ai/
|
|
22
|
+
"@alfe.ai/agent-api-client": "0.15.0",
|
|
23
|
+
"@alfe.ai/config": "0.4.1",
|
|
24
|
+
"@alfe.ai/mcp-bundler": "0.4.1"
|
|
24
25
|
},
|
|
25
26
|
"license": "UNLICENSED",
|
|
26
27
|
"homepage": "https://alfe.ai",
|
|
@@ -36,6 +37,7 @@
|
|
|
36
37
|
"scripts": {
|
|
37
38
|
"build": "tsdown",
|
|
38
39
|
"dev": "tsdown --watch",
|
|
40
|
+
"test": "vitest run",
|
|
39
41
|
"typecheck": "tsc --noEmit",
|
|
40
42
|
"lint": "eslint ."
|
|
41
43
|
}
|