@alfe.ai/xero-mcp 0.3.21 → 0.4.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/child-shim.cjs +102 -0
- package/dist/child-shim.d.cts +35 -0
- package/dist/child-shim.d.ts +35 -0
- package/dist/child-shim.js +100 -0
- package/dist/server.cjs +48 -18
- package/dist/server.d.cts +10 -3
- package/dist/server.d.ts +10 -3
- package/dist/server.js +48 -19
- package/package.json +1 -1
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
let node_fs = require("node:fs");
|
|
4
|
+
let node_module = require("node:module");
|
|
5
|
+
let node_path = require("node:path");
|
|
6
|
+
let node_url = require("node:url");
|
|
7
|
+
//#region src/child-shim.ts
|
|
8
|
+
/**
|
|
9
|
+
* Child bootstrap shim (runs INSIDE the spawned child process).
|
|
10
|
+
*
|
|
11
|
+
* The official `@xeroapi/xero-mcp-server` has no organisation selector: its
|
|
12
|
+
* `MCPXeroClient.updateTenants()` unconditionally assigns
|
|
13
|
+
* `this.tenantId = this.tenants[0].tenantId`, so a bearer token spanning several
|
|
14
|
+
* Xero organisations silently acts on whichever one Xero happens to return
|
|
15
|
+
* first. Every one of the child's tools resolves its organisation through that
|
|
16
|
+
* single field (`xeroClient.tenantId`), so pinning it here bounds the whole
|
|
17
|
+
* catalog.
|
|
18
|
+
*
|
|
19
|
+
* This shim imports the child's client module, re-pins `tenantId` to the
|
|
20
|
+
* explicitly selected organisation after each `updateTenants()`, and only then
|
|
21
|
+
* starts the child's real entrypoint.
|
|
22
|
+
*
|
|
23
|
+
* FAIL-CLOSED: every failure path throws instead of falling through to the
|
|
24
|
+
* child's first-organisation default. A missing selector, an unrecognised
|
|
25
|
+
* module shape, or a selector absent from the grant must stop the child, never
|
|
26
|
+
* silently target another organisation's books. Writing to the wrong company's
|
|
27
|
+
* ledger is the exact risk this boundary exists to prevent.
|
|
28
|
+
*/
|
|
29
|
+
const CHILD_PACKAGE = "@xeroapi/xero-mcp-server";
|
|
30
|
+
function isRecord(value) {
|
|
31
|
+
return typeof value === "object" && value !== null;
|
|
32
|
+
}
|
|
33
|
+
function isPatchableClient(value) {
|
|
34
|
+
return isRecord(value) && typeof value.updateTenants === "function";
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Resolve the child package root the same way the parent proxy does
|
|
38
|
+
* (`require.resolve` + `realpathSync`) so this shim and the child's own
|
|
39
|
+
* internal relative imports land on one module instance — and therefore one
|
|
40
|
+
* `xeroClient` singleton. A divergent specifier would yield a second, unpatched
|
|
41
|
+
* instance and defeat the pin.
|
|
42
|
+
*/
|
|
43
|
+
function resolveChildRoot() {
|
|
44
|
+
return (0, node_fs.realpathSync)((0, node_path.dirname)((0, node_module.createRequire)(require("url").pathToFileURL(__filename).href).resolve(`${CHILD_PACKAGE}/package.json`)));
|
|
45
|
+
}
|
|
46
|
+
function childModuleUrl(root, relativePath) {
|
|
47
|
+
return (0, node_url.pathToFileURL)((0, node_path.join)(root, relativePath)).href;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Compared case-insensitively to match `validateXeroTenantId` in `boundary.ts`,
|
|
51
|
+
* which lowercases. Xero's `/connections` body is untouched by the child, so a
|
|
52
|
+
* non-lowercase UUID would otherwise verify in the parent and throw here.
|
|
53
|
+
*/
|
|
54
|
+
function assertTenantInGrant(tenants, selectedTenantId) {
|
|
55
|
+
const list = Array.isArray(tenants) ? tenants : [];
|
|
56
|
+
const wanted = selectedTenantId.toLowerCase();
|
|
57
|
+
if (!list.some((entry) => isRecord(entry) && typeof entry.tenantId === "string" && entry.tenantId.toLowerCase() === wanted)) throw new Error(`Xero grant does not expose organisation ${selectedTenantId}; refusing to start`);
|
|
58
|
+
}
|
|
59
|
+
async function main() {
|
|
60
|
+
const selectedTenantId = process.env.XERO_TENANT_ID;
|
|
61
|
+
if (selectedTenantId === void 0 || selectedTenantId === "") throw new Error("XERO_TENANT_ID is required; refusing to start without an explicit organisation");
|
|
62
|
+
const root = resolveChildRoot();
|
|
63
|
+
const clientModule = await import(childModuleUrl(root, "dist/clients/xero-client.js"));
|
|
64
|
+
if (!isRecord(clientModule) || !isPatchableClient(clientModule.xeroClient)) throw new Error(`${CHILD_PACKAGE} did not export a patchable xeroClient; refusing to start`);
|
|
65
|
+
installTenantPin(clientModule.xeroClient, selectedTenantId);
|
|
66
|
+
await import(childModuleUrl(root, "dist/index.js"));
|
|
67
|
+
}
|
|
68
|
+
function installTenantPin(client, selectedTenantId) {
|
|
69
|
+
Object.defineProperty(client, "tenantId", {
|
|
70
|
+
get: () => selectedTenantId,
|
|
71
|
+
set: () => {},
|
|
72
|
+
configurable: false,
|
|
73
|
+
enumerable: true
|
|
74
|
+
});
|
|
75
|
+
const updateTenants = client.updateTenants.bind(client);
|
|
76
|
+
client.updateTenants = async () => {
|
|
77
|
+
const tenants = await updateTenants(false);
|
|
78
|
+
assertTenantInGrant(tenants, selectedTenantId);
|
|
79
|
+
return tenants;
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Mirrors `isProcessEntrypoint` in `server.ts` rather than importing it: this
|
|
84
|
+
* module runs inside the spawned child and must not pull the proxy (and its
|
|
85
|
+
* AgentApiClient/config dependencies) in with it. Keeping the import inert also
|
|
86
|
+
* lets the pin helpers be unit-tested without starting a child.
|
|
87
|
+
*/
|
|
88
|
+
function isProcessEntrypoint(argvPath, metaUrl) {
|
|
89
|
+
try {
|
|
90
|
+
if (argvPath === void 0 || argvPath === "") return false;
|
|
91
|
+
return (0, node_url.pathToFileURL)((0, node_fs.realpathSync)(argvPath)).href === (0, node_url.pathToFileURL)((0, node_fs.realpathSync)((0, node_url.fileURLToPath)(metaUrl))).href;
|
|
92
|
+
} catch {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (isProcessEntrypoint(process.argv[1], require("url").pathToFileURL(__filename).href)) main().catch((error) => {
|
|
97
|
+
console.error(`xero-mcp child shim: ${error instanceof Error ? error.message : String(error)}`);
|
|
98
|
+
process.exit(1);
|
|
99
|
+
});
|
|
100
|
+
//#endregion
|
|
101
|
+
exports.assertTenantInGrant = assertTenantInGrant;
|
|
102
|
+
exports.installTenantPin = installTenantPin;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
//#region src/child-shim.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Child bootstrap shim (runs INSIDE the spawned child process).
|
|
4
|
+
*
|
|
5
|
+
* The official `@xeroapi/xero-mcp-server` has no organisation selector: its
|
|
6
|
+
* `MCPXeroClient.updateTenants()` unconditionally assigns
|
|
7
|
+
* `this.tenantId = this.tenants[0].tenantId`, so a bearer token spanning several
|
|
8
|
+
* Xero organisations silently acts on whichever one Xero happens to return
|
|
9
|
+
* first. Every one of the child's tools resolves its organisation through that
|
|
10
|
+
* single field (`xeroClient.tenantId`), so pinning it here bounds the whole
|
|
11
|
+
* catalog.
|
|
12
|
+
*
|
|
13
|
+
* This shim imports the child's client module, re-pins `tenantId` to the
|
|
14
|
+
* explicitly selected organisation after each `updateTenants()`, and only then
|
|
15
|
+
* starts the child's real entrypoint.
|
|
16
|
+
*
|
|
17
|
+
* FAIL-CLOSED: every failure path throws instead of falling through to the
|
|
18
|
+
* child's first-organisation default. A missing selector, an unrecognised
|
|
19
|
+
* module shape, or a selector absent from the grant must stop the child, never
|
|
20
|
+
* silently target another organisation's books. Writing to the wrong company's
|
|
21
|
+
* ledger is the exact risk this boundary exists to prevent.
|
|
22
|
+
*/
|
|
23
|
+
interface PatchableClient {
|
|
24
|
+
tenantId: string;
|
|
25
|
+
updateTenants: (fullOrgDetails?: unknown) => Promise<unknown>;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Compared case-insensitively to match `validateXeroTenantId` in `boundary.ts`,
|
|
29
|
+
* which lowercases. Xero's `/connections` body is untouched by the child, so a
|
|
30
|
+
* non-lowercase UUID would otherwise verify in the parent and throw here.
|
|
31
|
+
*/
|
|
32
|
+
declare function assertTenantInGrant(tenants: unknown, selectedTenantId: string): void;
|
|
33
|
+
declare function installTenantPin(client: PatchableClient, selectedTenantId: string): void;
|
|
34
|
+
//#endregion
|
|
35
|
+
export { assertTenantInGrant, installTenantPin };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
//#region src/child-shim.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Child bootstrap shim (runs INSIDE the spawned child process).
|
|
4
|
+
*
|
|
5
|
+
* The official `@xeroapi/xero-mcp-server` has no organisation selector: its
|
|
6
|
+
* `MCPXeroClient.updateTenants()` unconditionally assigns
|
|
7
|
+
* `this.tenantId = this.tenants[0].tenantId`, so a bearer token spanning several
|
|
8
|
+
* Xero organisations silently acts on whichever one Xero happens to return
|
|
9
|
+
* first. Every one of the child's tools resolves its organisation through that
|
|
10
|
+
* single field (`xeroClient.tenantId`), so pinning it here bounds the whole
|
|
11
|
+
* catalog.
|
|
12
|
+
*
|
|
13
|
+
* This shim imports the child's client module, re-pins `tenantId` to the
|
|
14
|
+
* explicitly selected organisation after each `updateTenants()`, and only then
|
|
15
|
+
* starts the child's real entrypoint.
|
|
16
|
+
*
|
|
17
|
+
* FAIL-CLOSED: every failure path throws instead of falling through to the
|
|
18
|
+
* child's first-organisation default. A missing selector, an unrecognised
|
|
19
|
+
* module shape, or a selector absent from the grant must stop the child, never
|
|
20
|
+
* silently target another organisation's books. Writing to the wrong company's
|
|
21
|
+
* ledger is the exact risk this boundary exists to prevent.
|
|
22
|
+
*/
|
|
23
|
+
interface PatchableClient {
|
|
24
|
+
tenantId: string;
|
|
25
|
+
updateTenants: (fullOrgDetails?: unknown) => Promise<unknown>;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Compared case-insensitively to match `validateXeroTenantId` in `boundary.ts`,
|
|
29
|
+
* which lowercases. Xero's `/connections` body is untouched by the child, so a
|
|
30
|
+
* non-lowercase UUID would otherwise verify in the parent and throw here.
|
|
31
|
+
*/
|
|
32
|
+
declare function assertTenantInGrant(tenants: unknown, selectedTenantId: string): void;
|
|
33
|
+
declare function installTenantPin(client: PatchableClient, selectedTenantId: string): void;
|
|
34
|
+
//#endregion
|
|
35
|
+
export { assertTenantInGrant, installTenantPin };
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { realpathSync } from "node:fs";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
6
|
+
//#region src/child-shim.ts
|
|
7
|
+
/**
|
|
8
|
+
* Child bootstrap shim (runs INSIDE the spawned child process).
|
|
9
|
+
*
|
|
10
|
+
* The official `@xeroapi/xero-mcp-server` has no organisation selector: its
|
|
11
|
+
* `MCPXeroClient.updateTenants()` unconditionally assigns
|
|
12
|
+
* `this.tenantId = this.tenants[0].tenantId`, so a bearer token spanning several
|
|
13
|
+
* Xero organisations silently acts on whichever one Xero happens to return
|
|
14
|
+
* first. Every one of the child's tools resolves its organisation through that
|
|
15
|
+
* single field (`xeroClient.tenantId`), so pinning it here bounds the whole
|
|
16
|
+
* catalog.
|
|
17
|
+
*
|
|
18
|
+
* This shim imports the child's client module, re-pins `tenantId` to the
|
|
19
|
+
* explicitly selected organisation after each `updateTenants()`, and only then
|
|
20
|
+
* starts the child's real entrypoint.
|
|
21
|
+
*
|
|
22
|
+
* FAIL-CLOSED: every failure path throws instead of falling through to the
|
|
23
|
+
* child's first-organisation default. A missing selector, an unrecognised
|
|
24
|
+
* module shape, or a selector absent from the grant must stop the child, never
|
|
25
|
+
* silently target another organisation's books. Writing to the wrong company's
|
|
26
|
+
* ledger is the exact risk this boundary exists to prevent.
|
|
27
|
+
*/
|
|
28
|
+
const CHILD_PACKAGE = "@xeroapi/xero-mcp-server";
|
|
29
|
+
function isRecord(value) {
|
|
30
|
+
return typeof value === "object" && value !== null;
|
|
31
|
+
}
|
|
32
|
+
function isPatchableClient(value) {
|
|
33
|
+
return isRecord(value) && typeof value.updateTenants === "function";
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Resolve the child package root the same way the parent proxy does
|
|
37
|
+
* (`require.resolve` + `realpathSync`) so this shim and the child's own
|
|
38
|
+
* internal relative imports land on one module instance — and therefore one
|
|
39
|
+
* `xeroClient` singleton. A divergent specifier would yield a second, unpatched
|
|
40
|
+
* instance and defeat the pin.
|
|
41
|
+
*/
|
|
42
|
+
function resolveChildRoot() {
|
|
43
|
+
return realpathSync(dirname(createRequire(import.meta.url).resolve(`${CHILD_PACKAGE}/package.json`)));
|
|
44
|
+
}
|
|
45
|
+
function childModuleUrl(root, relativePath) {
|
|
46
|
+
return pathToFileURL(join(root, relativePath)).href;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Compared case-insensitively to match `validateXeroTenantId` in `boundary.ts`,
|
|
50
|
+
* which lowercases. Xero's `/connections` body is untouched by the child, so a
|
|
51
|
+
* non-lowercase UUID would otherwise verify in the parent and throw here.
|
|
52
|
+
*/
|
|
53
|
+
function assertTenantInGrant(tenants, selectedTenantId) {
|
|
54
|
+
const list = Array.isArray(tenants) ? tenants : [];
|
|
55
|
+
const wanted = selectedTenantId.toLowerCase();
|
|
56
|
+
if (!list.some((entry) => isRecord(entry) && typeof entry.tenantId === "string" && entry.tenantId.toLowerCase() === wanted)) throw new Error(`Xero grant does not expose organisation ${selectedTenantId}; refusing to start`);
|
|
57
|
+
}
|
|
58
|
+
async function main() {
|
|
59
|
+
const selectedTenantId = process.env.XERO_TENANT_ID;
|
|
60
|
+
if (selectedTenantId === void 0 || selectedTenantId === "") throw new Error("XERO_TENANT_ID is required; refusing to start without an explicit organisation");
|
|
61
|
+
const root = resolveChildRoot();
|
|
62
|
+
const clientModule = await import(childModuleUrl(root, "dist/clients/xero-client.js"));
|
|
63
|
+
if (!isRecord(clientModule) || !isPatchableClient(clientModule.xeroClient)) throw new Error(`${CHILD_PACKAGE} did not export a patchable xeroClient; refusing to start`);
|
|
64
|
+
installTenantPin(clientModule.xeroClient, selectedTenantId);
|
|
65
|
+
await import(childModuleUrl(root, "dist/index.js"));
|
|
66
|
+
}
|
|
67
|
+
function installTenantPin(client, selectedTenantId) {
|
|
68
|
+
Object.defineProperty(client, "tenantId", {
|
|
69
|
+
get: () => selectedTenantId,
|
|
70
|
+
set: () => {},
|
|
71
|
+
configurable: false,
|
|
72
|
+
enumerable: true
|
|
73
|
+
});
|
|
74
|
+
const updateTenants = client.updateTenants.bind(client);
|
|
75
|
+
client.updateTenants = async () => {
|
|
76
|
+
const tenants = await updateTenants(false);
|
|
77
|
+
assertTenantInGrant(tenants, selectedTenantId);
|
|
78
|
+
return tenants;
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Mirrors `isProcessEntrypoint` in `server.ts` rather than importing it: this
|
|
83
|
+
* module runs inside the spawned child and must not pull the proxy (and its
|
|
84
|
+
* AgentApiClient/config dependencies) in with it. Keeping the import inert also
|
|
85
|
+
* lets the pin helpers be unit-tested without starting a child.
|
|
86
|
+
*/
|
|
87
|
+
function isProcessEntrypoint(argvPath, metaUrl) {
|
|
88
|
+
try {
|
|
89
|
+
if (argvPath === void 0 || argvPath === "") return false;
|
|
90
|
+
return pathToFileURL(realpathSync(argvPath)).href === pathToFileURL(realpathSync(fileURLToPath(metaUrl))).href;
|
|
91
|
+
} catch {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (isProcessEntrypoint(process.argv[1], import.meta.url)) main().catch((error) => {
|
|
96
|
+
console.error(`xero-mcp child shim: ${error instanceof Error ? error.message : String(error)}`);
|
|
97
|
+
process.exit(1);
|
|
98
|
+
});
|
|
99
|
+
//#endregion
|
|
100
|
+
export { assertTenantInGrant, installTenantPin };
|
package/dist/server.cjs
CHANGED
|
@@ -51,8 +51,9 @@ const CHILD_ENV_ALLOWLIST = [
|
|
|
51
51
|
"https_proxy",
|
|
52
52
|
"no_proxy"
|
|
53
53
|
];
|
|
54
|
-
function buildChildEnvironment(accessToken, source = process.env) {
|
|
54
|
+
function buildChildEnvironment(accessToken, xeroTenantId, source = process.env) {
|
|
55
55
|
const token = validateAccessToken(accessToken, false);
|
|
56
|
+
const selectedTenantId = validateXeroTenantId(xeroTenantId);
|
|
56
57
|
const environment = Object.create(null);
|
|
57
58
|
for (const name of CHILD_ENV_ALLOWLIST) {
|
|
58
59
|
const value = source[name];
|
|
@@ -60,7 +61,8 @@ function buildChildEnvironment(accessToken, source = process.env) {
|
|
|
60
61
|
}
|
|
61
62
|
return {
|
|
62
63
|
...environment,
|
|
63
|
-
XERO_CLIENT_BEARER_TOKEN: token
|
|
64
|
+
XERO_CLIENT_BEARER_TOKEN: token,
|
|
65
|
+
XERO_TENANT_ID: selectedTenantId
|
|
64
66
|
};
|
|
65
67
|
}
|
|
66
68
|
function normalizeXeroAccount(value) {
|
|
@@ -176,10 +178,10 @@ async function verifyXeroTenantAccess(accessToken, xeroTenantId, fetchFn = fetch
|
|
|
176
178
|
throw new Error("Xero connections response was invalid JSON");
|
|
177
179
|
}
|
|
178
180
|
if (!Array.isArray(parsed) || parsed.length > 50) throw new Error("Xero connections response was invalid");
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
181
|
+
return parsed.map((entry) => {
|
|
182
|
+
if (!isRecord(entry)) throw new Error("Xero connection entry was invalid");
|
|
183
|
+
return validateXeroTenantId(entry.tenantId);
|
|
184
|
+
}).includes(selectedTenantId) ? "verified" : "mismatch";
|
|
183
185
|
}
|
|
184
186
|
function snapshotToolCatalog(tools) {
|
|
185
187
|
return tools.map((tool) => structuredClone(tool));
|
|
@@ -367,9 +369,14 @@ function isRecord(value) {
|
|
|
367
369
|
* Xero MCP proxy (Pattern A multi-connection boundary).
|
|
368
370
|
*
|
|
369
371
|
* The official child chooses the first organisation returned by Xero and does
|
|
370
|
-
* not consume XERO_TENANT_ID.
|
|
371
|
-
*
|
|
372
|
-
* organisation
|
|
372
|
+
* not consume XERO_TENANT_ID. Children are therefore launched through
|
|
373
|
+
* `child-shim.ts`, which pins the child's `tenantId` to the explicitly selected
|
|
374
|
+
* organisation before starting it, and fails closed if it cannot.
|
|
375
|
+
*
|
|
376
|
+
* One OAuth grant may cover several organisations, so the proxy runs one child
|
|
377
|
+
* per selected organisation off a single shared token, keyed by xeroTenantId. A
|
|
378
|
+
* bounded /connections preflight proves the selected organisation is still in
|
|
379
|
+
* the grant before spawning; the first entry is never assumed.
|
|
373
380
|
*/
|
|
374
381
|
const require$1 = (0, node_module.createRequire)(require("url").pathToFileURL(__filename).href);
|
|
375
382
|
const CHILD_PACKAGE = "@xeroapi/xero-mcp-server";
|
|
@@ -380,6 +387,23 @@ const SERVER_VERSION = validateSemver("xero-mcp package version", packageMetadat
|
|
|
380
387
|
const CHILD_VERSION = validateExactChildVersion(packageMetadata.dependencies?.[CHILD_PACKAGE], childMetadata.version);
|
|
381
388
|
const CHILD_BIN_PATH = resolveChildBin(childPackagePath, childMetadata.bin);
|
|
382
389
|
const CHILD_PACKAGE_ROOT = (0, node_fs.realpathSync)((0, node_path.dirname)(childPackagePath));
|
|
390
|
+
/**
|
|
391
|
+
* The child is launched through our own shim rather than `CHILD_BIN_PATH`
|
|
392
|
+
* directly: the shim pins the organisation before starting the child's real
|
|
393
|
+
* entrypoint (see `child-shim.ts`). It is emitted alongside this module, so it
|
|
394
|
+
* resolves relative to the built file.
|
|
395
|
+
*/
|
|
396
|
+
const CHILD_SHIM_PATH = (0, node_url.fileURLToPath)(new URL("./child-shim.js", require("url").pathToFileURL(__filename).href));
|
|
397
|
+
/**
|
|
398
|
+
* Ceiling on concurrently-running child processes, independent of
|
|
399
|
+
* `MAX_XERO_ACCOUNTS` (which bounds the accounts payload). Since one grant can
|
|
400
|
+
* now cover many organisations and each gets its own child, this bounds memory
|
|
401
|
+
* on the agent VM: a warmed child costs roughly 125 MB RSS, so an unbounded
|
|
402
|
+
* fan-out would OOM a 4 GB box that is also running the runtime and other MCP
|
|
403
|
+
* servers. Organisations beyond the ceiling are reported as `child_limit` by
|
|
404
|
+
* `xero_list_accounts` rather than dropped silently.
|
|
405
|
+
*/
|
|
406
|
+
const MAX_CONCURRENT_CHILDREN = 8;
|
|
383
407
|
const CHILD_START_TIMEOUT_MS = 3e4;
|
|
384
408
|
const CHILD_CATALOG_TIMEOUT_MS = 15e3;
|
|
385
409
|
const CHILD_CALL_TIMEOUT_MS = 6e4;
|
|
@@ -530,6 +554,11 @@ var XeroRuntime = class {
|
|
|
530
554
|
this.allAccountsSnapshot.push(summary(account, false, "missing_access_token"));
|
|
531
555
|
continue;
|
|
532
556
|
}
|
|
557
|
+
if (this.tenants.size >= MAX_CONCURRENT_CHILDREN) {
|
|
558
|
+
log(`Child ceiling of ${String(MAX_CONCURRENT_CHILDREN)} reached; skipping Xero connection ${connectionLogId(account.connectionId)}`);
|
|
559
|
+
this.allAccountsSnapshot.push(summary(account, false, "child_limit"));
|
|
560
|
+
continue;
|
|
561
|
+
}
|
|
533
562
|
const controller = new AbortController();
|
|
534
563
|
let verification;
|
|
535
564
|
try {
|
|
@@ -541,11 +570,11 @@ var XeroRuntime = class {
|
|
|
541
570
|
continue;
|
|
542
571
|
}
|
|
543
572
|
if (verification !== "verified") {
|
|
544
|
-
this.allAccountsSnapshot.push(summary(account, false,
|
|
573
|
+
this.allAccountsSnapshot.push(summary(account, false, "tenant_mismatch"));
|
|
545
574
|
continue;
|
|
546
575
|
}
|
|
547
576
|
try {
|
|
548
|
-
const client = await withDeadline(this.spawnChild(account.accessToken, controller.signal), CHILD_START_TIMEOUT_MS, "Xero child startup");
|
|
577
|
+
const client = await withDeadline(this.spawnChild(account.accessToken, account.xeroTenantId, controller.signal), CHILD_START_TIMEOUT_MS, "Xero child startup");
|
|
549
578
|
if (this.closed) {
|
|
550
579
|
await closeChild(client);
|
|
551
580
|
throw new Error("Xero runtime closed during child startup");
|
|
@@ -577,7 +606,7 @@ var XeroRuntime = class {
|
|
|
577
606
|
const tenant = this.tenants.get(xeroTenantId);
|
|
578
607
|
if (tenant !== void 0) return tenant;
|
|
579
608
|
const known = this.allAccountsSnapshot.find((candidate) => candidate.xeroTenantId === xeroTenantId);
|
|
580
|
-
if (known !== void 0) throw new Error(`xeroTenantId ${xeroTenantId} is connected but unavailable (${known.reason ?? "unknown"}).
|
|
609
|
+
if (known !== void 0) throw new Error(`xeroTenantId ${xeroTenantId} is connected but unavailable (${known.reason ?? "unknown"}). Call xero_list_accounts for the organisations that are usable right now. If this one is missing, the Xero grant no longer covers it — ask the user to reconnect Xero and authorise this organisation.`);
|
|
581
610
|
throw new Error(`Unknown xeroTenantId: ${xeroTenantId}. Call xero_list_accounts to see the connected Xero organisations on this agent.`);
|
|
582
611
|
}
|
|
583
612
|
async callChild(tenant, name, forwarded) {
|
|
@@ -608,11 +637,11 @@ var XeroRuntime = class {
|
|
|
608
637
|
}
|
|
609
638
|
if (verification !== "verified") {
|
|
610
639
|
controller.abort();
|
|
611
|
-
throw new Error("Refreshed Xero token
|
|
640
|
+
throw new Error("Refreshed Xero token no longer exposes the selected organisation");
|
|
612
641
|
}
|
|
613
642
|
let nextClient;
|
|
614
643
|
try {
|
|
615
|
-
nextClient = await withDeadline(this.spawnChild(accessToken, controller.signal), CHILD_START_TIMEOUT_MS, "Xero refreshed child startup");
|
|
644
|
+
nextClient = await withDeadline(this.spawnChild(accessToken, current.xeroTenantId, controller.signal), CHILD_START_TIMEOUT_MS, "Xero refreshed child startup");
|
|
616
645
|
} catch (error) {
|
|
617
646
|
controller.abort(error);
|
|
618
647
|
throw error;
|
|
@@ -678,7 +707,7 @@ function appendLocalTools(tools) {
|
|
|
678
707
|
},
|
|
679
708
|
{
|
|
680
709
|
name: "xero_refresh_token",
|
|
681
|
-
description: "Refresh one selected Xero OAuth credential and replace only its child process after the refreshed token is verified to
|
|
710
|
+
description: "Refresh one selected Xero OAuth credential and replace only its child process after the refreshed token is verified to still cover that organisation.",
|
|
682
711
|
inputSchema: {
|
|
683
712
|
type: "object",
|
|
684
713
|
properties: { xeroTenantId: {
|
|
@@ -699,11 +728,11 @@ function appendLocalTools(tools) {
|
|
|
699
728
|
}
|
|
700
729
|
];
|
|
701
730
|
}
|
|
702
|
-
async function spawnOfficialChild(accessToken, signal) {
|
|
731
|
+
async function spawnOfficialChild(accessToken, xeroTenantId, signal) {
|
|
703
732
|
const transport = new _modelcontextprotocol_sdk_client_stdio_js.StdioClientTransport({
|
|
704
733
|
command: process.execPath,
|
|
705
|
-
args: [
|
|
706
|
-
env: buildChildEnvironment(accessToken),
|
|
734
|
+
args: [CHILD_SHIM_PATH],
|
|
735
|
+
env: buildChildEnvironment(accessToken, xeroTenantId),
|
|
707
736
|
cwd: CHILD_PACKAGE_ROOT,
|
|
708
737
|
stderr: "ignore"
|
|
709
738
|
});
|
|
@@ -848,6 +877,7 @@ if (isProcessEntrypoint(process.argv[1], require("url").pathToFileURL(__filename
|
|
|
848
877
|
//#endregion
|
|
849
878
|
exports.CHILD_BIN_PATH = CHILD_BIN_PATH;
|
|
850
879
|
exports.CHILD_PACKAGE_ROOT = CHILD_PACKAGE_ROOT;
|
|
880
|
+
exports.CHILD_SHIM_PATH = CHILD_SHIM_PATH;
|
|
851
881
|
exports.CHILD_VERSION = CHILD_VERSION;
|
|
852
882
|
exports.SERVER_VERSION = SERVER_VERSION;
|
|
853
883
|
exports.XeroRuntime = XeroRuntime;
|
package/dist/server.d.cts
CHANGED
|
@@ -29,7 +29,7 @@ interface ProxiedTool {
|
|
|
29
29
|
annotations?: ChildToolDescriptor["annotations"];
|
|
30
30
|
execution?: ChildToolDescriptor["execution"];
|
|
31
31
|
}
|
|
32
|
-
type XeroTenantVerification = "verified" | "
|
|
32
|
+
type XeroTenantVerification = "verified" | "mismatch";
|
|
33
33
|
type JsonPrimitive = null | boolean | number | string;
|
|
34
34
|
type JsonValue = JsonPrimitive | JsonValue[] | {
|
|
35
35
|
[key: string]: JsonValue;
|
|
@@ -40,6 +40,13 @@ declare const SERVER_VERSION: string;
|
|
|
40
40
|
declare const CHILD_VERSION: string;
|
|
41
41
|
declare const CHILD_BIN_PATH: string;
|
|
42
42
|
declare const CHILD_PACKAGE_ROOT: string;
|
|
43
|
+
/**
|
|
44
|
+
* The child is launched through our own shim rather than `CHILD_BIN_PATH`
|
|
45
|
+
* directly: the shim pins the organisation before starting the child's real
|
|
46
|
+
* entrypoint (see `child-shim.ts`). It is emitted alongside this module, so it
|
|
47
|
+
* resolves relative to the built file.
|
|
48
|
+
*/
|
|
49
|
+
declare const CHILD_SHIM_PATH: string;
|
|
43
50
|
interface XeroApiClient {
|
|
44
51
|
getXeroAccounts(): Promise<unknown>;
|
|
45
52
|
refreshXeroAccountToken(accountIdentifier: string): Promise<unknown>;
|
|
@@ -55,7 +62,7 @@ interface XeroChildClient {
|
|
|
55
62
|
close(): Promise<void>;
|
|
56
63
|
}
|
|
57
64
|
interface XeroProxyOptions {
|
|
58
|
-
spawnChild?: (accessToken: string, signal: AbortSignal) => Promise<XeroChildClient>;
|
|
65
|
+
spawnChild?: (accessToken: string, xeroTenantId: string, signal: AbortSignal) => Promise<XeroChildClient>;
|
|
59
66
|
verifyTenantAccess?: (accessToken: string, xeroTenantId: string, signal: AbortSignal) => Promise<XeroTenantVerification>;
|
|
60
67
|
scheduleRefresh?: boolean;
|
|
61
68
|
}
|
|
@@ -105,4 +112,4 @@ interface RunningXeroProxy {
|
|
|
105
112
|
declare function startServer(apiClient?: XeroApiClient, options?: XeroProxyOptions): Promise<RunningXeroProxy>;
|
|
106
113
|
declare function isProcessEntrypoint(argvPath: string | undefined, metaUrl: string): boolean;
|
|
107
114
|
//#endregion
|
|
108
|
-
export { CHILD_BIN_PATH, CHILD_PACKAGE_ROOT, CHILD_VERSION, RunningXeroProxy, SERVER_VERSION, XeroApiClient, XeroChildClient, XeroProxyOptions, XeroRuntime, createProxyServer, isProcessEntrypoint, startServer };
|
|
115
|
+
export { CHILD_BIN_PATH, CHILD_PACKAGE_ROOT, CHILD_SHIM_PATH, CHILD_VERSION, RunningXeroProxy, SERVER_VERSION, XeroApiClient, XeroChildClient, XeroProxyOptions, XeroRuntime, createProxyServer, isProcessEntrypoint, startServer };
|
package/dist/server.d.ts
CHANGED
|
@@ -29,7 +29,7 @@ interface ProxiedTool {
|
|
|
29
29
|
annotations?: ChildToolDescriptor["annotations"];
|
|
30
30
|
execution?: ChildToolDescriptor["execution"];
|
|
31
31
|
}
|
|
32
|
-
type XeroTenantVerification = "verified" | "
|
|
32
|
+
type XeroTenantVerification = "verified" | "mismatch";
|
|
33
33
|
type JsonPrimitive = null | boolean | number | string;
|
|
34
34
|
type JsonValue = JsonPrimitive | JsonValue[] | {
|
|
35
35
|
[key: string]: JsonValue;
|
|
@@ -40,6 +40,13 @@ declare const SERVER_VERSION: string;
|
|
|
40
40
|
declare const CHILD_VERSION: string;
|
|
41
41
|
declare const CHILD_BIN_PATH: string;
|
|
42
42
|
declare const CHILD_PACKAGE_ROOT: string;
|
|
43
|
+
/**
|
|
44
|
+
* The child is launched through our own shim rather than `CHILD_BIN_PATH`
|
|
45
|
+
* directly: the shim pins the organisation before starting the child's real
|
|
46
|
+
* entrypoint (see `child-shim.ts`). It is emitted alongside this module, so it
|
|
47
|
+
* resolves relative to the built file.
|
|
48
|
+
*/
|
|
49
|
+
declare const CHILD_SHIM_PATH: string;
|
|
43
50
|
interface XeroApiClient {
|
|
44
51
|
getXeroAccounts(): Promise<unknown>;
|
|
45
52
|
refreshXeroAccountToken(accountIdentifier: string): Promise<unknown>;
|
|
@@ -55,7 +62,7 @@ interface XeroChildClient {
|
|
|
55
62
|
close(): Promise<void>;
|
|
56
63
|
}
|
|
57
64
|
interface XeroProxyOptions {
|
|
58
|
-
spawnChild?: (accessToken: string, signal: AbortSignal) => Promise<XeroChildClient>;
|
|
65
|
+
spawnChild?: (accessToken: string, xeroTenantId: string, signal: AbortSignal) => Promise<XeroChildClient>;
|
|
59
66
|
verifyTenantAccess?: (accessToken: string, xeroTenantId: string, signal: AbortSignal) => Promise<XeroTenantVerification>;
|
|
60
67
|
scheduleRefresh?: boolean;
|
|
61
68
|
}
|
|
@@ -105,4 +112,4 @@ interface RunningXeroProxy {
|
|
|
105
112
|
declare function startServer(apiClient?: XeroApiClient, options?: XeroProxyOptions): Promise<RunningXeroProxy>;
|
|
106
113
|
declare function isProcessEntrypoint(argvPath: string | undefined, metaUrl: string): boolean;
|
|
107
114
|
//#endregion
|
|
108
|
-
export { CHILD_BIN_PATH, CHILD_PACKAGE_ROOT, CHILD_VERSION, RunningXeroProxy, SERVER_VERSION, XeroApiClient, XeroChildClient, XeroProxyOptions, XeroRuntime, createProxyServer, isProcessEntrypoint, startServer };
|
|
115
|
+
export { CHILD_BIN_PATH, CHILD_PACKAGE_ROOT, CHILD_SHIM_PATH, CHILD_VERSION, RunningXeroProxy, SERVER_VERSION, XeroApiClient, XeroChildClient, XeroProxyOptions, XeroRuntime, createProxyServer, isProcessEntrypoint, startServer };
|
package/dist/server.js
CHANGED
|
@@ -50,8 +50,9 @@ const CHILD_ENV_ALLOWLIST = [
|
|
|
50
50
|
"https_proxy",
|
|
51
51
|
"no_proxy"
|
|
52
52
|
];
|
|
53
|
-
function buildChildEnvironment(accessToken, source = process.env) {
|
|
53
|
+
function buildChildEnvironment(accessToken, xeroTenantId, source = process.env) {
|
|
54
54
|
const token = validateAccessToken(accessToken, false);
|
|
55
|
+
const selectedTenantId = validateXeroTenantId(xeroTenantId);
|
|
55
56
|
const environment = Object.create(null);
|
|
56
57
|
for (const name of CHILD_ENV_ALLOWLIST) {
|
|
57
58
|
const value = source[name];
|
|
@@ -59,7 +60,8 @@ function buildChildEnvironment(accessToken, source = process.env) {
|
|
|
59
60
|
}
|
|
60
61
|
return {
|
|
61
62
|
...environment,
|
|
62
|
-
XERO_CLIENT_BEARER_TOKEN: token
|
|
63
|
+
XERO_CLIENT_BEARER_TOKEN: token,
|
|
64
|
+
XERO_TENANT_ID: selectedTenantId
|
|
63
65
|
};
|
|
64
66
|
}
|
|
65
67
|
function normalizeXeroAccount(value) {
|
|
@@ -175,10 +177,10 @@ async function verifyXeroTenantAccess(accessToken, xeroTenantId, fetchFn = fetch
|
|
|
175
177
|
throw new Error("Xero connections response was invalid JSON");
|
|
176
178
|
}
|
|
177
179
|
if (!Array.isArray(parsed) || parsed.length > 50) throw new Error("Xero connections response was invalid");
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
180
|
+
return parsed.map((entry) => {
|
|
181
|
+
if (!isRecord(entry)) throw new Error("Xero connection entry was invalid");
|
|
182
|
+
return validateXeroTenantId(entry.tenantId);
|
|
183
|
+
}).includes(selectedTenantId) ? "verified" : "mismatch";
|
|
182
184
|
}
|
|
183
185
|
function snapshotToolCatalog(tools) {
|
|
184
186
|
return tools.map((tool) => structuredClone(tool));
|
|
@@ -366,9 +368,14 @@ function isRecord(value) {
|
|
|
366
368
|
* Xero MCP proxy (Pattern A multi-connection boundary).
|
|
367
369
|
*
|
|
368
370
|
* The official child chooses the first organisation returned by Xero and does
|
|
369
|
-
* not consume XERO_TENANT_ID.
|
|
370
|
-
*
|
|
371
|
-
* organisation
|
|
371
|
+
* not consume XERO_TENANT_ID. Children are therefore launched through
|
|
372
|
+
* `child-shim.ts`, which pins the child's `tenantId` to the explicitly selected
|
|
373
|
+
* organisation before starting it, and fails closed if it cannot.
|
|
374
|
+
*
|
|
375
|
+
* One OAuth grant may cover several organisations, so the proxy runs one child
|
|
376
|
+
* per selected organisation off a single shared token, keyed by xeroTenantId. A
|
|
377
|
+
* bounded /connections preflight proves the selected organisation is still in
|
|
378
|
+
* the grant before spawning; the first entry is never assumed.
|
|
372
379
|
*/
|
|
373
380
|
const require = createRequire(import.meta.url);
|
|
374
381
|
const CHILD_PACKAGE = "@xeroapi/xero-mcp-server";
|
|
@@ -379,6 +386,23 @@ const SERVER_VERSION = validateSemver("xero-mcp package version", packageMetadat
|
|
|
379
386
|
const CHILD_VERSION = validateExactChildVersion(packageMetadata.dependencies?.[CHILD_PACKAGE], childMetadata.version);
|
|
380
387
|
const CHILD_BIN_PATH = resolveChildBin(childPackagePath, childMetadata.bin);
|
|
381
388
|
const CHILD_PACKAGE_ROOT = realpathSync(dirname(childPackagePath));
|
|
389
|
+
/**
|
|
390
|
+
* The child is launched through our own shim rather than `CHILD_BIN_PATH`
|
|
391
|
+
* directly: the shim pins the organisation before starting the child's real
|
|
392
|
+
* entrypoint (see `child-shim.ts`). It is emitted alongside this module, so it
|
|
393
|
+
* resolves relative to the built file.
|
|
394
|
+
*/
|
|
395
|
+
const CHILD_SHIM_PATH = fileURLToPath(new URL("./child-shim.js", import.meta.url));
|
|
396
|
+
/**
|
|
397
|
+
* Ceiling on concurrently-running child processes, independent of
|
|
398
|
+
* `MAX_XERO_ACCOUNTS` (which bounds the accounts payload). Since one grant can
|
|
399
|
+
* now cover many organisations and each gets its own child, this bounds memory
|
|
400
|
+
* on the agent VM: a warmed child costs roughly 125 MB RSS, so an unbounded
|
|
401
|
+
* fan-out would OOM a 4 GB box that is also running the runtime and other MCP
|
|
402
|
+
* servers. Organisations beyond the ceiling are reported as `child_limit` by
|
|
403
|
+
* `xero_list_accounts` rather than dropped silently.
|
|
404
|
+
*/
|
|
405
|
+
const MAX_CONCURRENT_CHILDREN = 8;
|
|
382
406
|
const CHILD_START_TIMEOUT_MS = 3e4;
|
|
383
407
|
const CHILD_CATALOG_TIMEOUT_MS = 15e3;
|
|
384
408
|
const CHILD_CALL_TIMEOUT_MS = 6e4;
|
|
@@ -529,6 +553,11 @@ var XeroRuntime = class {
|
|
|
529
553
|
this.allAccountsSnapshot.push(summary(account, false, "missing_access_token"));
|
|
530
554
|
continue;
|
|
531
555
|
}
|
|
556
|
+
if (this.tenants.size >= MAX_CONCURRENT_CHILDREN) {
|
|
557
|
+
log(`Child ceiling of ${String(MAX_CONCURRENT_CHILDREN)} reached; skipping Xero connection ${connectionLogId(account.connectionId)}`);
|
|
558
|
+
this.allAccountsSnapshot.push(summary(account, false, "child_limit"));
|
|
559
|
+
continue;
|
|
560
|
+
}
|
|
532
561
|
const controller = new AbortController();
|
|
533
562
|
let verification;
|
|
534
563
|
try {
|
|
@@ -540,11 +569,11 @@ var XeroRuntime = class {
|
|
|
540
569
|
continue;
|
|
541
570
|
}
|
|
542
571
|
if (verification !== "verified") {
|
|
543
|
-
this.allAccountsSnapshot.push(summary(account, false,
|
|
572
|
+
this.allAccountsSnapshot.push(summary(account, false, "tenant_mismatch"));
|
|
544
573
|
continue;
|
|
545
574
|
}
|
|
546
575
|
try {
|
|
547
|
-
const client = await withDeadline(this.spawnChild(account.accessToken, controller.signal), CHILD_START_TIMEOUT_MS, "Xero child startup");
|
|
576
|
+
const client = await withDeadline(this.spawnChild(account.accessToken, account.xeroTenantId, controller.signal), CHILD_START_TIMEOUT_MS, "Xero child startup");
|
|
548
577
|
if (this.closed) {
|
|
549
578
|
await closeChild(client);
|
|
550
579
|
throw new Error("Xero runtime closed during child startup");
|
|
@@ -576,7 +605,7 @@ var XeroRuntime = class {
|
|
|
576
605
|
const tenant = this.tenants.get(xeroTenantId);
|
|
577
606
|
if (tenant !== void 0) return tenant;
|
|
578
607
|
const known = this.allAccountsSnapshot.find((candidate) => candidate.xeroTenantId === xeroTenantId);
|
|
579
|
-
if (known !== void 0) throw new Error(`xeroTenantId ${xeroTenantId} is connected but unavailable (${known.reason ?? "unknown"}).
|
|
608
|
+
if (known !== void 0) throw new Error(`xeroTenantId ${xeroTenantId} is connected but unavailable (${known.reason ?? "unknown"}). Call xero_list_accounts for the organisations that are usable right now. If this one is missing, the Xero grant no longer covers it — ask the user to reconnect Xero and authorise this organisation.`);
|
|
580
609
|
throw new Error(`Unknown xeroTenantId: ${xeroTenantId}. Call xero_list_accounts to see the connected Xero organisations on this agent.`);
|
|
581
610
|
}
|
|
582
611
|
async callChild(tenant, name, forwarded) {
|
|
@@ -607,11 +636,11 @@ var XeroRuntime = class {
|
|
|
607
636
|
}
|
|
608
637
|
if (verification !== "verified") {
|
|
609
638
|
controller.abort();
|
|
610
|
-
throw new Error("Refreshed Xero token
|
|
639
|
+
throw new Error("Refreshed Xero token no longer exposes the selected organisation");
|
|
611
640
|
}
|
|
612
641
|
let nextClient;
|
|
613
642
|
try {
|
|
614
|
-
nextClient = await withDeadline(this.spawnChild(accessToken, controller.signal), CHILD_START_TIMEOUT_MS, "Xero refreshed child startup");
|
|
643
|
+
nextClient = await withDeadline(this.spawnChild(accessToken, current.xeroTenantId, controller.signal), CHILD_START_TIMEOUT_MS, "Xero refreshed child startup");
|
|
615
644
|
} catch (error) {
|
|
616
645
|
controller.abort(error);
|
|
617
646
|
throw error;
|
|
@@ -677,7 +706,7 @@ function appendLocalTools(tools) {
|
|
|
677
706
|
},
|
|
678
707
|
{
|
|
679
708
|
name: "xero_refresh_token",
|
|
680
|
-
description: "Refresh one selected Xero OAuth credential and replace only its child process after the refreshed token is verified to
|
|
709
|
+
description: "Refresh one selected Xero OAuth credential and replace only its child process after the refreshed token is verified to still cover that organisation.",
|
|
681
710
|
inputSchema: {
|
|
682
711
|
type: "object",
|
|
683
712
|
properties: { xeroTenantId: {
|
|
@@ -698,11 +727,11 @@ function appendLocalTools(tools) {
|
|
|
698
727
|
}
|
|
699
728
|
];
|
|
700
729
|
}
|
|
701
|
-
async function spawnOfficialChild(accessToken, signal) {
|
|
730
|
+
async function spawnOfficialChild(accessToken, xeroTenantId, signal) {
|
|
702
731
|
const transport = new StdioClientTransport({
|
|
703
732
|
command: process.execPath,
|
|
704
|
-
args: [
|
|
705
|
-
env: buildChildEnvironment(accessToken),
|
|
733
|
+
args: [CHILD_SHIM_PATH],
|
|
734
|
+
env: buildChildEnvironment(accessToken, xeroTenantId),
|
|
706
735
|
cwd: CHILD_PACKAGE_ROOT,
|
|
707
736
|
stderr: "ignore"
|
|
708
737
|
});
|
|
@@ -845,4 +874,4 @@ if (isProcessEntrypoint(process.argv[1], import.meta.url)) startServer().then((r
|
|
|
845
874
|
process.exitCode = 1;
|
|
846
875
|
});
|
|
847
876
|
//#endregion
|
|
848
|
-
export { CHILD_BIN_PATH, CHILD_PACKAGE_ROOT, CHILD_VERSION, SERVER_VERSION, XeroRuntime, createProxyServer, isProcessEntrypoint, startServer };
|
|
877
|
+
export { CHILD_BIN_PATH, CHILD_PACKAGE_ROOT, CHILD_SHIM_PATH, CHILD_VERSION, SERVER_VERSION, XeroRuntime, createProxyServer, isProcessEntrypoint, startServer };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alfe.ai/xero-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Xero MCP proxy server — bridges the official @xeroapi/xero-mcp-server with Alfe OAuth credentials and automatic token refresh",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/server.js",
|