@alfe.ai/xero-mcp 0.3.16 → 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 +32 -7
- package/dist/server.cjs +856 -0
- package/dist/server.d.cts +108 -0
- package/dist/server.d.ts +108 -1
- package/dist/server.js +811 -330
- package/package.json +11 -8
package/dist/server.cjs
ADDED
|
@@ -0,0 +1,856 @@
|
|
|
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
|
+
let _alfe_ai_agent_api_client = require("@alfe.ai/agent-api-client");
|
|
8
|
+
let _alfe_ai_config = require("@alfe.ai/config");
|
|
9
|
+
let _alfe_ai_mcp_bundler = require("@alfe.ai/mcp-bundler");
|
|
10
|
+
let _modelcontextprotocol_sdk_client_index_js = require("@modelcontextprotocol/sdk/client/index.js");
|
|
11
|
+
let _modelcontextprotocol_sdk_client_stdio_js = require("@modelcontextprotocol/sdk/client/stdio.js");
|
|
12
|
+
let _modelcontextprotocol_sdk_server_index_js = require("@modelcontextprotocol/sdk/server/index.js");
|
|
13
|
+
let _modelcontextprotocol_sdk_server_stdio_js = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
14
|
+
let _modelcontextprotocol_sdk_types_js = require("@modelcontextprotocol/sdk/types.js");
|
|
15
|
+
let node_crypto = require("node:crypto");
|
|
16
|
+
const MAX_FORWARDED_ARGUMENT_BYTES = 2 * 1024 * 1024;
|
|
17
|
+
const MAX_TOOL_RESULT_BYTES = 5 * 1024 * 1024;
|
|
18
|
+
const MAX_SCHEMA_BYTES = 768 * 1024;
|
|
19
|
+
const MAX_CONNECTIONS_RESPONSE_BYTES = 128 * 1024;
|
|
20
|
+
const MAX_JSON_DEPTH = 16;
|
|
21
|
+
const MAX_JSON_NODES = 1e5;
|
|
22
|
+
const MAX_JSON_ARRAY_ITEMS = 5e3;
|
|
23
|
+
const MAX_ARGUMENT_STRING_CHARS = 15e5;
|
|
24
|
+
const MAX_TOKEN_CHARS = 32768;
|
|
25
|
+
const MAX_ERROR_CHARS = 2048;
|
|
26
|
+
const XERO_CONNECTIONS_TIMEOUT_MS = 15e3;
|
|
27
|
+
const XERO_TENANT_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu;
|
|
28
|
+
const TOOL_NAME_PATTERN = /^[A-Za-z0-9_.:-]{1,128}$/u;
|
|
29
|
+
const UNSAFE_KEYS = new Set([
|
|
30
|
+
"__proto__",
|
|
31
|
+
"constructor",
|
|
32
|
+
"prototype"
|
|
33
|
+
]);
|
|
34
|
+
const RESERVED_TOOLS = new Set(["xero_list_accounts", "xero_refresh_token"]);
|
|
35
|
+
const CHILD_ENV_ALLOWLIST = [
|
|
36
|
+
"HOME",
|
|
37
|
+
"TMPDIR",
|
|
38
|
+
"TMP",
|
|
39
|
+
"TEMP",
|
|
40
|
+
"LANG",
|
|
41
|
+
"LC_ALL",
|
|
42
|
+
"LC_CTYPE",
|
|
43
|
+
"TZ",
|
|
44
|
+
"SSL_CERT_FILE",
|
|
45
|
+
"SSL_CERT_DIR",
|
|
46
|
+
"NODE_EXTRA_CA_CERTS",
|
|
47
|
+
"HTTP_PROXY",
|
|
48
|
+
"HTTPS_PROXY",
|
|
49
|
+
"NO_PROXY",
|
|
50
|
+
"http_proxy",
|
|
51
|
+
"https_proxy",
|
|
52
|
+
"no_proxy"
|
|
53
|
+
];
|
|
54
|
+
function buildChildEnvironment(accessToken, source = process.env) {
|
|
55
|
+
const token = validateAccessToken(accessToken, false);
|
|
56
|
+
const environment = Object.create(null);
|
|
57
|
+
for (const name of CHILD_ENV_ALLOWLIST) {
|
|
58
|
+
const value = source[name];
|
|
59
|
+
if (value !== void 0 && !containsControlCharacter(value)) environment[name] = value;
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
...environment,
|
|
63
|
+
XERO_CLIENT_BEARER_TOKEN: token
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function normalizeXeroAccount(value) {
|
|
67
|
+
if (!isRecord(value)) throw new Error("Xero account must be an object");
|
|
68
|
+
return {
|
|
69
|
+
connectionId: validateText("connectionId", value.connectionId, 128),
|
|
70
|
+
accountIdentifier: validateText("accountIdentifier", value.accountIdentifier, 512),
|
|
71
|
+
displayName: validateOptionalDisplayText("displayName", value.displayName, 512),
|
|
72
|
+
connectedAt: validateConnectedAt(value.connectedAt),
|
|
73
|
+
accessToken: validateAccessToken(value.accessToken, true),
|
|
74
|
+
xeroTenantId: validateXeroTenantId(value.xeroTenantId)
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function readXeroAccountsResponse(value) {
|
|
78
|
+
if (!isRecord(value) || !Array.isArray(value.accounts)) throw new Error("Xero accounts response must contain an accounts array");
|
|
79
|
+
if (value.accounts.length > 50) throw new Error(`Xero accounts response exceeds ${String(50)} accounts`);
|
|
80
|
+
return value.accounts;
|
|
81
|
+
}
|
|
82
|
+
function readRefreshedAccessToken(value) {
|
|
83
|
+
if (!isRecord(value)) throw new Error("Xero refresh response must be an object");
|
|
84
|
+
return validateAccessToken(value.accessToken, false);
|
|
85
|
+
}
|
|
86
|
+
function injectTenantSelector(tool) {
|
|
87
|
+
if (!isRecord(tool)) throw new Error("Child tool descriptor must be an object");
|
|
88
|
+
const name = validateToolName(tool.name);
|
|
89
|
+
if (RESERVED_TOOLS.has(name)) throw new Error(`Child tool collides with reserved proxy tool: ${name}`);
|
|
90
|
+
const original = cloneJsonRecord("tool input schema", tool.inputSchema, MAX_SCHEMA_BYTES, 5e5);
|
|
91
|
+
if (Object.hasOwn(original, "type") && original.type !== "object") throw new Error("Child tool input schema must describe an object");
|
|
92
|
+
const originalProperties = Object.hasOwn(original, "properties") ? cloneJsonRecord("tool schema properties", original.properties, MAX_SCHEMA_BYTES, 5e5) : Object.create(null);
|
|
93
|
+
const required = new Set(["xeroTenantId", ...validateRequired(original.required)]);
|
|
94
|
+
const result = {
|
|
95
|
+
name,
|
|
96
|
+
inputSchema: {
|
|
97
|
+
...original,
|
|
98
|
+
type: "object",
|
|
99
|
+
properties: {
|
|
100
|
+
...originalProperties,
|
|
101
|
+
xeroTenantId: {
|
|
102
|
+
type: "string",
|
|
103
|
+
pattern: XERO_TENANT_ID_PATTERN.source,
|
|
104
|
+
maxLength: 36,
|
|
105
|
+
description: "Xero organisation tenant ID from xero_list_accounts. This required selector chooses the exact connected OAuth organisation."
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
required: [...required]
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
if (tool.title !== void 0) result.title = validateText("tool title", tool.title, 256);
|
|
112
|
+
if (tool.description !== void 0) result.description = validateDocumentationText("tool description", tool.description, 2e4);
|
|
113
|
+
if (tool.outputSchema !== void 0) result.outputSchema = cloneJsonRecord("tool output schema", tool.outputSchema, MAX_SCHEMA_BYTES, 5e5);
|
|
114
|
+
if (tool.annotations !== void 0) result.annotations = validateAnnotations(tool.annotations);
|
|
115
|
+
if (tool.execution !== void 0) result.execution = validateExecution(tool.execution);
|
|
116
|
+
return result;
|
|
117
|
+
}
|
|
118
|
+
function validateToolCatalog(tools) {
|
|
119
|
+
if (!Array.isArray(tools)) throw new Error("Child tool catalog must be an array");
|
|
120
|
+
if (tools.length > 150) throw new Error(`Child tool catalog exceeds ${String(150)} tools`);
|
|
121
|
+
const names = /* @__PURE__ */ new Set();
|
|
122
|
+
return tools.map((tool) => {
|
|
123
|
+
const proxied = injectTenantSelector(tool);
|
|
124
|
+
if (names.has(proxied.name)) throw new Error(`Child tool catalog contains duplicate name: ${proxied.name}`);
|
|
125
|
+
names.add(proxied.name);
|
|
126
|
+
return proxied;
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
function prepareForwardedArguments(value) {
|
|
130
|
+
const cloned = cloneJsonRecord("tool arguments", value, MAX_FORWARDED_ARGUMENT_BYTES, MAX_ARGUMENT_STRING_CHARS);
|
|
131
|
+
const xeroTenantId = validateXeroTenantId(cloned.xeroTenantId);
|
|
132
|
+
const { xeroTenantId: _selector, ...forwarded } = cloned;
|
|
133
|
+
return {
|
|
134
|
+
xeroTenantId,
|
|
135
|
+
forwarded
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
function assertBoundedToolArguments(value) {
|
|
139
|
+
return cloneJsonRecord("tool arguments", value, MAX_FORWARDED_ARGUMENT_BYTES, MAX_ARGUMENT_STRING_CHARS);
|
|
140
|
+
}
|
|
141
|
+
function assertBoundedToolResult(value) {
|
|
142
|
+
return cloneJsonRecord("child tool result", value, MAX_TOOL_RESULT_BYTES, MAX_TOOL_RESULT_BYTES);
|
|
143
|
+
}
|
|
144
|
+
function childResultIsError(value) {
|
|
145
|
+
return value.isError === true;
|
|
146
|
+
}
|
|
147
|
+
function childResultIsUnauthorized(value) {
|
|
148
|
+
if (!childResultIsError(value)) return false;
|
|
149
|
+
const text = extractToolErrorText(value).toLowerCase();
|
|
150
|
+
return text.includes("401") || text.includes("unauthorized") || text.includes("authentication failed");
|
|
151
|
+
}
|
|
152
|
+
function extractToolErrorText(value) {
|
|
153
|
+
if (!Array.isArray(value.content)) return "";
|
|
154
|
+
return value.content.slice(0, 100).map((entry) => isRecord(entry) && typeof entry.text === "string" ? entry.text.slice(0, MAX_ERROR_CHARS) : "").join("\n").slice(0, MAX_ERROR_CHARS);
|
|
155
|
+
}
|
|
156
|
+
async function verifyXeroTenantAccess(accessToken, xeroTenantId, fetchFn = fetch, signal = AbortSignal.timeout(XERO_CONNECTIONS_TIMEOUT_MS)) {
|
|
157
|
+
const token = validateAccessToken(accessToken, false);
|
|
158
|
+
const selectedTenantId = validateXeroTenantId(xeroTenantId);
|
|
159
|
+
const response = await fetchFn("https://api.xero.com/connections", {
|
|
160
|
+
headers: {
|
|
161
|
+
Authorization: `Bearer ${token}`,
|
|
162
|
+
Accept: "application/json"
|
|
163
|
+
},
|
|
164
|
+
redirect: "error",
|
|
165
|
+
signal
|
|
166
|
+
});
|
|
167
|
+
if (!response.ok) {
|
|
168
|
+
await response.body?.cancel().catch(() => void 0);
|
|
169
|
+
throw new Error(`Xero connections request failed with HTTP ${String(response.status)}`);
|
|
170
|
+
}
|
|
171
|
+
const text = await readResponseText(response, MAX_CONNECTIONS_RESPONSE_BYTES);
|
|
172
|
+
let parsed;
|
|
173
|
+
try {
|
|
174
|
+
parsed = JSON.parse(text);
|
|
175
|
+
} catch {
|
|
176
|
+
throw new Error("Xero connections response was invalid JSON");
|
|
177
|
+
}
|
|
178
|
+
if (!Array.isArray(parsed) || parsed.length > 50) throw new Error("Xero connections response was invalid");
|
|
179
|
+
if (parsed.length !== 1) return "ambiguous";
|
|
180
|
+
const only = parsed[0];
|
|
181
|
+
if (!isRecord(only)) throw new Error("Xero connection entry was invalid");
|
|
182
|
+
return validateXeroTenantId(only.tenantId) === selectedTenantId ? "verified" : "mismatch";
|
|
183
|
+
}
|
|
184
|
+
function snapshotToolCatalog(tools) {
|
|
185
|
+
return tools.map((tool) => structuredClone(tool));
|
|
186
|
+
}
|
|
187
|
+
function safeErrorMessage(error, secrets = []) {
|
|
188
|
+
let message = (error instanceof Error ? error.message : String(error)).slice(0, MAX_ERROR_CHARS);
|
|
189
|
+
for (const secret of secrets) if (secret.length >= 4) message = message.split(secret).join("[REDACTED]");
|
|
190
|
+
return flattenControls(message).replace(/\b(Bearer|Basic)\s+[^\s,;]+/giu, "$1 [REDACTED]").replace(/\b(api[_-]?key|access[_-]?token|refresh[_-]?token|secret|password)\s*[=:]\s*[^\s,;]+/giu, "$1=[REDACTED]").replace(/https?:\/\/[^\s/@:]+:[^\s/@]+@/giu, "https://[REDACTED]@").replace(/\balfe_[A-Za-z0-9_-]{8,}/gu, "[REDACTED]").slice(0, MAX_ERROR_CHARS);
|
|
191
|
+
}
|
|
192
|
+
function connectionLogId(value) {
|
|
193
|
+
return (0, node_crypto.createHash)("sha256").update(value).digest("hex").slice(0, 12);
|
|
194
|
+
}
|
|
195
|
+
async function withDeadline(promise, timeoutMs, label) {
|
|
196
|
+
let timer;
|
|
197
|
+
try {
|
|
198
|
+
return await Promise.race([promise, new Promise((_resolve, reject) => {
|
|
199
|
+
timer = setTimeout(() => {
|
|
200
|
+
reject(/* @__PURE__ */ new Error(`${label} timed out`));
|
|
201
|
+
}, timeoutMs);
|
|
202
|
+
timer.unref();
|
|
203
|
+
})]);
|
|
204
|
+
} finally {
|
|
205
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
function validateXeroTenantId(value) {
|
|
209
|
+
if (typeof value !== "string" || !XERO_TENANT_ID_PATTERN.test(value)) throw new Error("xeroTenantId must be a canonical Xero organisation UUID");
|
|
210
|
+
return value.toLowerCase();
|
|
211
|
+
}
|
|
212
|
+
function validateAccessToken(value, allowEmpty) {
|
|
213
|
+
if (allowEmpty && value === "") return "";
|
|
214
|
+
if (typeof value !== "string" || value.length < 8 || value.length > MAX_TOKEN_CHARS || value !== value.trim() || containsControlCharacter(value)) throw new Error("Xero access token is invalid");
|
|
215
|
+
return value;
|
|
216
|
+
}
|
|
217
|
+
function validateConnectedAt(value) {
|
|
218
|
+
const checked = validateText("connectedAt", value, 64);
|
|
219
|
+
const parsed = new Date(checked);
|
|
220
|
+
if (Number.isNaN(parsed.valueOf()) || parsed.toISOString() !== checked) throw new Error("connectedAt must be a canonical ISO date-time");
|
|
221
|
+
return checked;
|
|
222
|
+
}
|
|
223
|
+
function validateToolName(value) {
|
|
224
|
+
if (typeof value !== "string" || !TOOL_NAME_PATTERN.test(value)) throw new Error("Child tool name is invalid");
|
|
225
|
+
return value;
|
|
226
|
+
}
|
|
227
|
+
function validateText(label, value, maxChars) {
|
|
228
|
+
if (typeof value !== "string" || value.length < 1 || value.length > maxChars || containsControlCharacter(value)) throw new Error(`${label} must contain 1 to ${String(maxChars)} non-control characters`);
|
|
229
|
+
return value;
|
|
230
|
+
}
|
|
231
|
+
function validateOptionalDisplayText(label, value, maxChars) {
|
|
232
|
+
if (value === null) return null;
|
|
233
|
+
if (typeof value !== "string" || value.length > maxChars || containsControlCharacter(value)) throw new Error(`${label} must be null or at most ${String(maxChars)} non-control characters`);
|
|
234
|
+
return value;
|
|
235
|
+
}
|
|
236
|
+
function validateDocumentationText(label, value, maxChars) {
|
|
237
|
+
if (typeof value !== "string" || value.length < 1 || value.length > maxChars || Array.from(value).some((character) => {
|
|
238
|
+
const codePoint = character.codePointAt(0) ?? 0;
|
|
239
|
+
return codePoint < 32 && character !== "\n" && character !== "\r" && character !== " " || codePoint === 127;
|
|
240
|
+
})) throw new Error(`${label} must contain 1 to ${String(maxChars)} safe characters`);
|
|
241
|
+
return value;
|
|
242
|
+
}
|
|
243
|
+
function validateRequired(value) {
|
|
244
|
+
if (value === void 0) return [];
|
|
245
|
+
if (!Array.isArray(value) || value.length > 150) throw new Error("tool schema required must be an array");
|
|
246
|
+
const result = [];
|
|
247
|
+
for (const entry of value) {
|
|
248
|
+
if (typeof entry !== "string" || entry.length < 1 || entry.length > 128) throw new Error("tool schema required contains an invalid property name");
|
|
249
|
+
if (!result.includes(entry)) result.push(entry);
|
|
250
|
+
}
|
|
251
|
+
return result;
|
|
252
|
+
}
|
|
253
|
+
function validateAnnotations(value) {
|
|
254
|
+
if (!isRecord(value)) throw new Error("tool annotations must be an object");
|
|
255
|
+
const result = {};
|
|
256
|
+
if (value.title !== void 0) result.title = validateText("annotation title", value.title, 256);
|
|
257
|
+
for (const key of [
|
|
258
|
+
"readOnlyHint",
|
|
259
|
+
"destructiveHint",
|
|
260
|
+
"idempotentHint",
|
|
261
|
+
"openWorldHint"
|
|
262
|
+
]) {
|
|
263
|
+
const hint = value[key];
|
|
264
|
+
if (hint !== void 0) {
|
|
265
|
+
if (typeof hint !== "boolean") throw new Error(`tool annotation ${key} must be boolean`);
|
|
266
|
+
result[key] = hint;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return result;
|
|
270
|
+
}
|
|
271
|
+
function validateExecution(value) {
|
|
272
|
+
if (!isRecord(value)) throw new Error("tool execution metadata must be an object");
|
|
273
|
+
const taskSupport = value.taskSupport;
|
|
274
|
+
if (taskSupport !== void 0 && taskSupport !== "optional" && taskSupport !== "required" && taskSupport !== "forbidden") throw new Error("tool execution taskSupport is invalid");
|
|
275
|
+
return taskSupport === void 0 ? {} : { taskSupport };
|
|
276
|
+
}
|
|
277
|
+
async function readResponseText(response, maxBytes) {
|
|
278
|
+
const declared = response.headers.get("content-length");
|
|
279
|
+
if (declared !== null && (!/^\d+$/u.test(declared) || Number(declared) > maxBytes)) {
|
|
280
|
+
await response.body?.cancel().catch(() => void 0);
|
|
281
|
+
throw new Error("Xero connections response exceeds the byte limit");
|
|
282
|
+
}
|
|
283
|
+
if (response.body === null) return "";
|
|
284
|
+
const reader = response.body.getReader();
|
|
285
|
+
const decoder = new TextDecoder();
|
|
286
|
+
let bytes = 0;
|
|
287
|
+
let text = "";
|
|
288
|
+
try {
|
|
289
|
+
for (;;) {
|
|
290
|
+
const result = await reader.read();
|
|
291
|
+
if (result.done) break;
|
|
292
|
+
if (result.value === void 0) throw new Error("Xero response stream returned no bytes");
|
|
293
|
+
bytes += result.value.byteLength;
|
|
294
|
+
if (bytes > maxBytes) {
|
|
295
|
+
await reader.cancel("response too large").catch(() => void 0);
|
|
296
|
+
throw new Error("Xero connections response exceeds the byte limit");
|
|
297
|
+
}
|
|
298
|
+
text += decoder.decode(result.value, { stream: true });
|
|
299
|
+
}
|
|
300
|
+
text += decoder.decode();
|
|
301
|
+
return text;
|
|
302
|
+
} finally {
|
|
303
|
+
reader.releaseLock();
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
function cloneJsonRecord(label, value, maxBytes, maxStringChars) {
|
|
307
|
+
if (!isRecord(value)) throw new Error(`${label} must be an object`);
|
|
308
|
+
const cloned = cloneRecord(label, value, 0, { nodes: 0 }, maxStringChars);
|
|
309
|
+
if (Buffer.byteLength(JSON.stringify(cloned), "utf8") > maxBytes) throw new Error(`${label} exceeds the ${String(maxBytes)} byte limit`);
|
|
310
|
+
return cloned;
|
|
311
|
+
}
|
|
312
|
+
function cloneJson(label, value, depth, budget, maxStringChars) {
|
|
313
|
+
budget.nodes += 1;
|
|
314
|
+
if (budget.nodes > MAX_JSON_NODES) throw new Error(`${label} contains too many values`);
|
|
315
|
+
if (depth > MAX_JSON_DEPTH) throw new Error(`${label} exceeds the JSON depth limit`);
|
|
316
|
+
if (value === null || typeof value === "boolean") return value;
|
|
317
|
+
if (typeof value === "number") {
|
|
318
|
+
if (!Number.isFinite(value)) throw new Error(`${label} contains a non-finite number`);
|
|
319
|
+
return value;
|
|
320
|
+
}
|
|
321
|
+
if (typeof value === "string") {
|
|
322
|
+
if (value.length > maxStringChars) throw new Error(`${label} contains an oversized string`);
|
|
323
|
+
return value;
|
|
324
|
+
}
|
|
325
|
+
if (Array.isArray(value)) {
|
|
326
|
+
if (value.length > MAX_JSON_ARRAY_ITEMS) throw new Error(`${label} contains an oversized array`);
|
|
327
|
+
return value.map((entry) => cloneJson(label, entry, depth + 1, budget, maxStringChars));
|
|
328
|
+
}
|
|
329
|
+
if (!isRecord(value)) throw new Error(`${label} contains a non-JSON value`);
|
|
330
|
+
return cloneRecord(label, value, depth, budget, maxStringChars);
|
|
331
|
+
}
|
|
332
|
+
function cloneRecord(label, value, depth, budget, maxStringChars) {
|
|
333
|
+
const result = Object.create(null);
|
|
334
|
+
for (const [key, descriptor] of Object.entries(Object.getOwnPropertyDescriptors(value))) {
|
|
335
|
+
if (key.length < 1 || key.length > 256 || UNSAFE_KEYS.has(key) || descriptor.get !== void 0 || descriptor.set !== void 0) throw new Error(`${label} contains an unsafe property`);
|
|
336
|
+
result[key] = cloneJson(label, descriptor.value, depth + 1, budget, maxStringChars);
|
|
337
|
+
}
|
|
338
|
+
return result;
|
|
339
|
+
}
|
|
340
|
+
function containsControlCharacter(value) {
|
|
341
|
+
return Array.from(value).some((character) => {
|
|
342
|
+
const codePoint = character.codePointAt(0) ?? 0;
|
|
343
|
+
return codePoint < 32 || codePoint === 127;
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
function flattenControls(value) {
|
|
347
|
+
let output = "";
|
|
348
|
+
let previousWasControl = false;
|
|
349
|
+
for (const character of value) {
|
|
350
|
+
const codePoint = character.codePointAt(0) ?? 0;
|
|
351
|
+
const isControl = codePoint < 32 || codePoint === 127;
|
|
352
|
+
if (isControl) {
|
|
353
|
+
if (!previousWasControl) output += " ";
|
|
354
|
+
} else output += character;
|
|
355
|
+
previousWasControl = isControl;
|
|
356
|
+
}
|
|
357
|
+
return output;
|
|
358
|
+
}
|
|
359
|
+
function isRecord(value) {
|
|
360
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
361
|
+
const prototype = Object.getPrototypeOf(value);
|
|
362
|
+
return prototype === Object.prototype || prototype === null;
|
|
363
|
+
}
|
|
364
|
+
//#endregion
|
|
365
|
+
//#region src/server.ts
|
|
366
|
+
/**
|
|
367
|
+
* Xero MCP proxy (Pattern A multi-connection boundary).
|
|
368
|
+
*
|
|
369
|
+
* The official child chooses the first organisation returned by Xero and does
|
|
370
|
+
* not consume XERO_TENANT_ID. This proxy therefore starts a child only after a
|
|
371
|
+
* bounded /connections preflight proves that the token exposes exactly one
|
|
372
|
+
* organisation and that it equals the explicit xeroTenantId selector.
|
|
373
|
+
*/
|
|
374
|
+
const require$1 = (0, node_module.createRequire)(require("url").pathToFileURL(__filename).href);
|
|
375
|
+
const CHILD_PACKAGE = "@xeroapi/xero-mcp-server";
|
|
376
|
+
const packageMetadata = require$1("../package.json");
|
|
377
|
+
const childPackagePath = require$1.resolve(`${CHILD_PACKAGE}/package.json`);
|
|
378
|
+
const childMetadata = require$1(childPackagePath);
|
|
379
|
+
const SERVER_VERSION = validateSemver("xero-mcp package version", packageMetadata.version);
|
|
380
|
+
const CHILD_VERSION = validateExactChildVersion(packageMetadata.dependencies?.[CHILD_PACKAGE], childMetadata.version);
|
|
381
|
+
const CHILD_BIN_PATH = resolveChildBin(childPackagePath, childMetadata.bin);
|
|
382
|
+
const CHILD_PACKAGE_ROOT = (0, node_fs.realpathSync)((0, node_path.dirname)(childPackagePath));
|
|
383
|
+
const CHILD_START_TIMEOUT_MS = 3e4;
|
|
384
|
+
const CHILD_CATALOG_TIMEOUT_MS = 15e3;
|
|
385
|
+
const CHILD_CALL_TIMEOUT_MS = 6e4;
|
|
386
|
+
const CHILD_CLOSE_TIMEOUT_MS = 5e3;
|
|
387
|
+
const TOKEN_REFRESH_TIMEOUT_MS = 3e4;
|
|
388
|
+
const REFRESH_INTERVAL_MS = 1500 * 1e3;
|
|
389
|
+
const REFRESH_RETRY_MS = 60 * 1e3;
|
|
390
|
+
var XeroRuntime = class {
|
|
391
|
+
tenants = /* @__PURE__ */ new Map();
|
|
392
|
+
claimedTenantIds = /* @__PURE__ */ new Map();
|
|
393
|
+
allAccountsSnapshot = [];
|
|
394
|
+
catalogToolNames = /* @__PURE__ */ new Set();
|
|
395
|
+
inFlightRefreshByTenant = /* @__PURE__ */ new Map();
|
|
396
|
+
cachedTools = [];
|
|
397
|
+
refreshTimer;
|
|
398
|
+
closePromise;
|
|
399
|
+
closed = false;
|
|
400
|
+
spawnChild;
|
|
401
|
+
verifyTenant;
|
|
402
|
+
shouldScheduleRefresh;
|
|
403
|
+
constructor(apiClient, options = {}) {
|
|
404
|
+
this.apiClient = apiClient;
|
|
405
|
+
this.spawnChild = options.spawnChild ?? spawnOfficialChild;
|
|
406
|
+
this.verifyTenant = options.verifyTenantAccess ?? ((accessToken, xeroTenantId, signal) => verifyXeroTenantAccess(accessToken, xeroTenantId, fetch, signal));
|
|
407
|
+
this.shouldScheduleRefresh = options.scheduleRefresh ?? true;
|
|
408
|
+
}
|
|
409
|
+
async initialize() {
|
|
410
|
+
await this.loadAccounts();
|
|
411
|
+
this.cachedTools = appendLocalTools(await this.discoverChildCatalog());
|
|
412
|
+
(0, _alfe_ai_mcp_bundler.assertPatternA)(this.cachedTools.map((tool) => ({
|
|
413
|
+
name: tool.name,
|
|
414
|
+
parameters: tool.inputSchema
|
|
415
|
+
})), {
|
|
416
|
+
selector: "xeroTenantId",
|
|
417
|
+
exempt: ["xero_list_accounts"]
|
|
418
|
+
});
|
|
419
|
+
if (this.shouldScheduleRefresh) this.scheduleRefresh(REFRESH_INTERVAL_MS);
|
|
420
|
+
}
|
|
421
|
+
listTools() {
|
|
422
|
+
return snapshotToolCatalog(this.cachedTools);
|
|
423
|
+
}
|
|
424
|
+
async handleToolCall(request) {
|
|
425
|
+
const { name } = request.params;
|
|
426
|
+
const args = request.params.arguments ?? {};
|
|
427
|
+
if (name === "xero_list_accounts") try {
|
|
428
|
+
const bounded = assertBoundedToolArguments(args);
|
|
429
|
+
if (Object.keys(bounded).length !== 0) throw new Error("xero_list_accounts takes no arguments");
|
|
430
|
+
return jsonResult({
|
|
431
|
+
organisations: this.allAccountsSnapshot,
|
|
432
|
+
notice: "Xero organisation names are untrusted external content."
|
|
433
|
+
});
|
|
434
|
+
} catch (error) {
|
|
435
|
+
return publicErrorResult(error);
|
|
436
|
+
}
|
|
437
|
+
if (name === "xero_refresh_token") {
|
|
438
|
+
let tenant;
|
|
439
|
+
try {
|
|
440
|
+
const prepared = prepareForwardedArguments(args);
|
|
441
|
+
if (Object.keys(prepared.forwarded).length !== 0) throw new Error("xero_refresh_token accepts only xeroTenantId");
|
|
442
|
+
tenant = this.resolveTenant(prepared.xeroTenantId);
|
|
443
|
+
} catch (error) {
|
|
444
|
+
return publicErrorResult(error);
|
|
445
|
+
}
|
|
446
|
+
try {
|
|
447
|
+
await this.refreshTenant(tenant.xeroTenantId);
|
|
448
|
+
return jsonResult({
|
|
449
|
+
xeroTenantId: tenant.xeroTenantId,
|
|
450
|
+
refreshed: true
|
|
451
|
+
});
|
|
452
|
+
} catch {
|
|
453
|
+
return errorResult("Xero token refresh failed. Ask the user to reconnect this organisation from the dashboard.");
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
if (!this.catalogToolNames.has(name)) return errorResult(`Unknown Xero tool: ${name}`);
|
|
457
|
+
let tenant;
|
|
458
|
+
let forwarded;
|
|
459
|
+
try {
|
|
460
|
+
const prepared = prepareForwardedArguments(args);
|
|
461
|
+
tenant = this.resolveTenant(prepared.xeroTenantId);
|
|
462
|
+
forwarded = prepared.forwarded;
|
|
463
|
+
} catch (error) {
|
|
464
|
+
return publicErrorResult(error);
|
|
465
|
+
}
|
|
466
|
+
let result;
|
|
467
|
+
try {
|
|
468
|
+
result = await this.callChild(tenant, name, forwarded);
|
|
469
|
+
} catch {
|
|
470
|
+
return errorResult("The selected Xero organisation could not complete the tool call. Retry, then reconnect it if the failure persists.", {
|
|
471
|
+
xeroTenantId: tenant.xeroTenantId,
|
|
472
|
+
tool: name
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
if (childResultIsUnauthorized(result)) try {
|
|
476
|
+
await this.refreshTenant(tenant.xeroTenantId);
|
|
477
|
+
const refreshed = this.resolveTenant(tenant.xeroTenantId);
|
|
478
|
+
result = await this.callChild(refreshed, name, forwarded);
|
|
479
|
+
} catch {
|
|
480
|
+
return errorResult("Xero authentication failed and the credential could not be refreshed. Ask the user to reconnect this organisation from the dashboard.", {
|
|
481
|
+
xeroTenantId: tenant.xeroTenantId,
|
|
482
|
+
tool: name
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
if (childResultIsError(result)) return errorResult(extractToolErrorText(result) || "Xero rejected the tool request", {
|
|
486
|
+
xeroTenantId: tenant.xeroTenantId,
|
|
487
|
+
tool: name
|
|
488
|
+
}, [tenant.accessToken]);
|
|
489
|
+
return result;
|
|
490
|
+
}
|
|
491
|
+
async close() {
|
|
492
|
+
if (this.closePromise !== void 0) return this.closePromise;
|
|
493
|
+
this.closed = true;
|
|
494
|
+
if (this.refreshTimer !== void 0) clearTimeout(this.refreshTimer);
|
|
495
|
+
this.refreshTimer = void 0;
|
|
496
|
+
this.closePromise = (async () => {
|
|
497
|
+
const current = [...this.tenants.values()];
|
|
498
|
+
this.tenants.clear();
|
|
499
|
+
await Promise.all(current.map(async ({ client }) => closeChild(client)));
|
|
500
|
+
await Promise.allSettled([...this.inFlightRefreshByTenant.values()]);
|
|
501
|
+
})();
|
|
502
|
+
return this.closePromise;
|
|
503
|
+
}
|
|
504
|
+
async loadAccounts() {
|
|
505
|
+
const rawAccounts = readXeroAccountsResponse(await this.apiClient.getXeroAccounts());
|
|
506
|
+
if (rawAccounts.length === 0) log("No Xero organisations connected; starting in discovery-only mode");
|
|
507
|
+
for (const rawAccount of rawAccounts) {
|
|
508
|
+
let account;
|
|
509
|
+
try {
|
|
510
|
+
account = normalizeXeroAccount(rawAccount);
|
|
511
|
+
} catch (error) {
|
|
512
|
+
log(`Rejected invalid Xero connection ${typeof rawAccount.connectionId === "string" ? connectionLogId(rawAccount.connectionId) : "invalid"}: ${safeErrorMessage(error)}`);
|
|
513
|
+
this.allAccountsSnapshot.push({
|
|
514
|
+
xeroTenantId: null,
|
|
515
|
+
displayName: null,
|
|
516
|
+
connectedAt: null,
|
|
517
|
+
connected: false,
|
|
518
|
+
reason: "invalid_account"
|
|
519
|
+
});
|
|
520
|
+
continue;
|
|
521
|
+
}
|
|
522
|
+
if (this.claimedTenantIds.has(account.xeroTenantId)) {
|
|
523
|
+
log(`Rejected duplicate Xero tenant from connection ${connectionLogId(account.connectionId)}`);
|
|
524
|
+
this.allAccountsSnapshot.push(summary(account, false, "duplicate_tenant"));
|
|
525
|
+
continue;
|
|
526
|
+
}
|
|
527
|
+
this.claimedTenantIds.set(account.xeroTenantId, account.connectionId);
|
|
528
|
+
if (account.accessToken.length === 0) {
|
|
529
|
+
log(`Missing token for Xero connection ${connectionLogId(account.connectionId)}`);
|
|
530
|
+
this.allAccountsSnapshot.push(summary(account, false, "missing_access_token"));
|
|
531
|
+
continue;
|
|
532
|
+
}
|
|
533
|
+
const controller = new AbortController();
|
|
534
|
+
let verification;
|
|
535
|
+
try {
|
|
536
|
+
verification = await withDeadline(this.verifyTenant(account.accessToken, account.xeroTenantId, controller.signal), CHILD_START_TIMEOUT_MS, "Xero tenant verification");
|
|
537
|
+
} catch (error) {
|
|
538
|
+
controller.abort(error);
|
|
539
|
+
log(`Tenant verification failed for Xero connection ${connectionLogId(account.connectionId)}: ${safeErrorMessage(error, [account.accessToken])}`);
|
|
540
|
+
this.allAccountsSnapshot.push(summary(account, false, "verification_failed"));
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
if (verification !== "verified") {
|
|
544
|
+
this.allAccountsSnapshot.push(summary(account, false, verification === "ambiguous" ? "ambiguous_token" : "tenant_mismatch"));
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
try {
|
|
548
|
+
const client = await withDeadline(this.spawnChild(account.accessToken, controller.signal), CHILD_START_TIMEOUT_MS, "Xero child startup");
|
|
549
|
+
if (this.closed) {
|
|
550
|
+
await closeChild(client);
|
|
551
|
+
throw new Error("Xero runtime closed during child startup");
|
|
552
|
+
}
|
|
553
|
+
this.tenants.set(account.xeroTenantId, {
|
|
554
|
+
...account,
|
|
555
|
+
client
|
|
556
|
+
});
|
|
557
|
+
this.allAccountsSnapshot.push(summary(account, true));
|
|
558
|
+
log(`Spawned child for Xero connection ${connectionLogId(account.connectionId)}`);
|
|
559
|
+
} catch (error) {
|
|
560
|
+
controller.abort(error);
|
|
561
|
+
log(`Child startup failed for Xero connection ${connectionLogId(account.connectionId)}: ${safeErrorMessage(error, [account.accessToken])}`);
|
|
562
|
+
this.allAccountsSnapshot.push(summary(account, false, "spawn_failed"));
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
async discoverChildCatalog() {
|
|
567
|
+
for (const tenant of this.tenants.values()) try {
|
|
568
|
+
const tools = validateToolCatalog((await withDeadline(tenant.client.listTools(), CHILD_CATALOG_TIMEOUT_MS, "Xero child tool discovery")).tools);
|
|
569
|
+
for (const tool of tools) this.catalogToolNames.add(tool.name);
|
|
570
|
+
return tools;
|
|
571
|
+
} catch (error) {
|
|
572
|
+
log(`Tool discovery failed for Xero connection ${connectionLogId(tenant.connectionId)}: ${safeErrorMessage(error, [tenant.accessToken])}`);
|
|
573
|
+
}
|
|
574
|
+
return [];
|
|
575
|
+
}
|
|
576
|
+
resolveTenant(xeroTenantId) {
|
|
577
|
+
const tenant = this.tenants.get(xeroTenantId);
|
|
578
|
+
if (tenant !== void 0) return tenant;
|
|
579
|
+
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"}). Ask the user to reconnect an OAuth credential that exposes exactly this one Xero organisation.`);
|
|
581
|
+
throw new Error(`Unknown xeroTenantId: ${xeroTenantId}. Call xero_list_accounts to see the connected Xero organisations on this agent.`);
|
|
582
|
+
}
|
|
583
|
+
async callChild(tenant, name, forwarded) {
|
|
584
|
+
return assertBoundedToolResult(await withDeadline(tenant.client.callTool({
|
|
585
|
+
name,
|
|
586
|
+
arguments: forwarded
|
|
587
|
+
}), CHILD_CALL_TIMEOUT_MS, `Xero ${name}`));
|
|
588
|
+
}
|
|
589
|
+
refreshTenant(xeroTenantId) {
|
|
590
|
+
const inFlight = this.inFlightRefreshByTenant.get(xeroTenantId);
|
|
591
|
+
if (inFlight !== void 0) return inFlight;
|
|
592
|
+
const current = this.resolveTenant(xeroTenantId);
|
|
593
|
+
const promise = this.refreshTenantInner(current).finally(() => {
|
|
594
|
+
if (this.inFlightRefreshByTenant.get(xeroTenantId) === promise) this.inFlightRefreshByTenant.delete(xeroTenantId);
|
|
595
|
+
});
|
|
596
|
+
this.inFlightRefreshByTenant.set(xeroTenantId, promise);
|
|
597
|
+
return promise;
|
|
598
|
+
}
|
|
599
|
+
async refreshTenantInner(current) {
|
|
600
|
+
const accessToken = readRefreshedAccessToken(await withDeadline(this.apiClient.refreshXeroAccountToken(current.accountIdentifier), TOKEN_REFRESH_TIMEOUT_MS, "Xero token refresh"));
|
|
601
|
+
const controller = new AbortController();
|
|
602
|
+
let verification;
|
|
603
|
+
try {
|
|
604
|
+
verification = await withDeadline(this.verifyTenant(accessToken, current.xeroTenantId, controller.signal), CHILD_START_TIMEOUT_MS, "Xero refreshed tenant verification");
|
|
605
|
+
} catch (error) {
|
|
606
|
+
controller.abort(error);
|
|
607
|
+
throw error;
|
|
608
|
+
}
|
|
609
|
+
if (verification !== "verified") {
|
|
610
|
+
controller.abort();
|
|
611
|
+
throw new Error("Refreshed Xero token did not map uniquely to the selected organisation");
|
|
612
|
+
}
|
|
613
|
+
let nextClient;
|
|
614
|
+
try {
|
|
615
|
+
nextClient = await withDeadline(this.spawnChild(accessToken, controller.signal), CHILD_START_TIMEOUT_MS, "Xero refreshed child startup");
|
|
616
|
+
} catch (error) {
|
|
617
|
+
controller.abort(error);
|
|
618
|
+
throw error;
|
|
619
|
+
}
|
|
620
|
+
if (this.closed || this.tenants.get(current.xeroTenantId) !== current) {
|
|
621
|
+
await closeChild(nextClient);
|
|
622
|
+
throw new Error("Xero tenant lifecycle changed during refresh");
|
|
623
|
+
}
|
|
624
|
+
this.tenants.set(current.xeroTenantId, {
|
|
625
|
+
...current,
|
|
626
|
+
accessToken,
|
|
627
|
+
client: nextClient
|
|
628
|
+
});
|
|
629
|
+
await closeChild(current.client);
|
|
630
|
+
}
|
|
631
|
+
scheduleRefresh(delayMs) {
|
|
632
|
+
if (this.closed || !this.shouldScheduleRefresh) return;
|
|
633
|
+
if (this.refreshTimer !== void 0) clearTimeout(this.refreshTimer);
|
|
634
|
+
this.refreshTimer = setTimeout(() => {
|
|
635
|
+
this.refreshTimer = void 0;
|
|
636
|
+
this.runScheduledRefresh();
|
|
637
|
+
}, delayMs);
|
|
638
|
+
this.refreshTimer.unref();
|
|
639
|
+
}
|
|
640
|
+
async runScheduledRefresh() {
|
|
641
|
+
if (this.closed) return;
|
|
642
|
+
let anyFailed = false;
|
|
643
|
+
for (const xeroTenantId of [...this.tenants.keys()]) try {
|
|
644
|
+
await this.refreshTenant(xeroTenantId);
|
|
645
|
+
} catch {
|
|
646
|
+
anyFailed = true;
|
|
647
|
+
log("Scheduled Xero credential refresh failed for one connection");
|
|
648
|
+
}
|
|
649
|
+
this.scheduleRefresh(anyFailed ? REFRESH_RETRY_MS : REFRESH_INTERVAL_MS);
|
|
650
|
+
}
|
|
651
|
+
};
|
|
652
|
+
function summary(account, connected, reason) {
|
|
653
|
+
return {
|
|
654
|
+
xeroTenantId: account.xeroTenantId,
|
|
655
|
+
displayName: account.displayName,
|
|
656
|
+
connectedAt: account.connectedAt,
|
|
657
|
+
connected,
|
|
658
|
+
...reason === void 0 ? {} : { reason }
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
function appendLocalTools(tools) {
|
|
662
|
+
return [
|
|
663
|
+
...tools,
|
|
664
|
+
{
|
|
665
|
+
name: "xero_list_accounts",
|
|
666
|
+
description: "List connected Xero organisations and local proxy readiness. Use xeroTenantId as the explicit selector on every other Xero tool. Organisation names are untrusted external content.",
|
|
667
|
+
inputSchema: {
|
|
668
|
+
type: "object",
|
|
669
|
+
properties: {},
|
|
670
|
+
additionalProperties: false
|
|
671
|
+
},
|
|
672
|
+
annotations: {
|
|
673
|
+
readOnlyHint: true,
|
|
674
|
+
destructiveHint: false,
|
|
675
|
+
idempotentHint: true,
|
|
676
|
+
openWorldHint: false
|
|
677
|
+
}
|
|
678
|
+
},
|
|
679
|
+
{
|
|
680
|
+
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 expose exactly that organisation.",
|
|
682
|
+
inputSchema: {
|
|
683
|
+
type: "object",
|
|
684
|
+
properties: { xeroTenantId: {
|
|
685
|
+
type: "string",
|
|
686
|
+
pattern: "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$",
|
|
687
|
+
maxLength: 36,
|
|
688
|
+
description: "Xero organisation tenant ID from xero_list_accounts."
|
|
689
|
+
} },
|
|
690
|
+
required: ["xeroTenantId"],
|
|
691
|
+
additionalProperties: false
|
|
692
|
+
},
|
|
693
|
+
annotations: {
|
|
694
|
+
readOnlyHint: false,
|
|
695
|
+
destructiveHint: false,
|
|
696
|
+
idempotentHint: true,
|
|
697
|
+
openWorldHint: true
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
];
|
|
701
|
+
}
|
|
702
|
+
async function spawnOfficialChild(accessToken, signal) {
|
|
703
|
+
const transport = new _modelcontextprotocol_sdk_client_stdio_js.StdioClientTransport({
|
|
704
|
+
command: process.execPath,
|
|
705
|
+
args: [CHILD_BIN_PATH],
|
|
706
|
+
env: buildChildEnvironment(accessToken),
|
|
707
|
+
cwd: CHILD_PACKAGE_ROOT,
|
|
708
|
+
stderr: "ignore"
|
|
709
|
+
});
|
|
710
|
+
const client = new _modelcontextprotocol_sdk_client_index_js.Client({
|
|
711
|
+
name: "xero-mcp-proxy",
|
|
712
|
+
version: SERVER_VERSION
|
|
713
|
+
});
|
|
714
|
+
const abort = () => {
|
|
715
|
+
closeChild(client);
|
|
716
|
+
};
|
|
717
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
718
|
+
try {
|
|
719
|
+
await client.connect(transport);
|
|
720
|
+
if (signal.aborted) throw new Error("Xero child startup was aborted");
|
|
721
|
+
return client;
|
|
722
|
+
} catch (error) {
|
|
723
|
+
await closeChild(client);
|
|
724
|
+
throw error;
|
|
725
|
+
} finally {
|
|
726
|
+
signal.removeEventListener("abort", abort);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
async function closeChild(client) {
|
|
730
|
+
await withDeadline(client.close(), CHILD_CLOSE_TIMEOUT_MS, "Xero child close").catch(() => void 0);
|
|
731
|
+
}
|
|
732
|
+
async function createProxyServer(apiClient, options = {}) {
|
|
733
|
+
const runtime = new XeroRuntime(apiClient, options);
|
|
734
|
+
try {
|
|
735
|
+
await runtime.initialize();
|
|
736
|
+
} catch (error) {
|
|
737
|
+
await runtime.close();
|
|
738
|
+
throw error;
|
|
739
|
+
}
|
|
740
|
+
const server = new _modelcontextprotocol_sdk_server_index_js.Server({
|
|
741
|
+
name: "xero-mcp-proxy",
|
|
742
|
+
version: SERVER_VERSION
|
|
743
|
+
}, { capabilities: { tools: {} } });
|
|
744
|
+
server.setRequestHandler(_modelcontextprotocol_sdk_types_js.ListToolsRequestSchema, () => ({ tools: runtime.listTools() }));
|
|
745
|
+
server.setRequestHandler(_modelcontextprotocol_sdk_types_js.CallToolRequestSchema, (request) => runtime.handleToolCall(request));
|
|
746
|
+
return {
|
|
747
|
+
server,
|
|
748
|
+
runtime
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
async function startServer(apiClient, options = {}) {
|
|
752
|
+
let client = apiClient;
|
|
753
|
+
if (client === void 0) {
|
|
754
|
+
const config = (0, _alfe_ai_config.resolveConfig)();
|
|
755
|
+
client = new _alfe_ai_agent_api_client.AgentApiClient({
|
|
756
|
+
apiKey: config.apiKey,
|
|
757
|
+
apiUrl: config.apiUrl
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
const { server, runtime } = await createProxyServer(client, options);
|
|
761
|
+
let closePromise;
|
|
762
|
+
const close = () => {
|
|
763
|
+
closePromise ??= Promise.all([runtime.close(), withDeadline(server.close(), CHILD_CLOSE_TIMEOUT_MS, "Xero proxy close").catch(() => void 0)]).then(() => void 0);
|
|
764
|
+
return closePromise;
|
|
765
|
+
};
|
|
766
|
+
try {
|
|
767
|
+
await server.connect(new _modelcontextprotocol_sdk_server_stdio_js.StdioServerTransport());
|
|
768
|
+
log(`Proxy running with ${String(runtime.listTools().length - 2)} child tool(s)`);
|
|
769
|
+
return {
|
|
770
|
+
server,
|
|
771
|
+
runtime,
|
|
772
|
+
close
|
|
773
|
+
};
|
|
774
|
+
} catch (error) {
|
|
775
|
+
await close();
|
|
776
|
+
throw error;
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
function isProcessEntrypoint(argvPath, metaUrl) {
|
|
780
|
+
try {
|
|
781
|
+
if (!argvPath) return false;
|
|
782
|
+
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;
|
|
783
|
+
} catch {
|
|
784
|
+
return false;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
function resolveChildBin(packagePath, bin) {
|
|
788
|
+
let relativeBin;
|
|
789
|
+
if (typeof bin === "string") relativeBin = bin;
|
|
790
|
+
else if (bin !== null && typeof bin === "object" && !Array.isArray(bin)) relativeBin = bin["xero-mcp-server"];
|
|
791
|
+
if (typeof relativeBin !== "string" || relativeBin.length === 0 || (0, node_path.isAbsolute)(relativeBin)) throw new Error(`${CHILD_PACKAGE} package bin path is invalid`);
|
|
792
|
+
const packageRoot = (0, node_fs.realpathSync)((0, node_path.dirname)(packagePath));
|
|
793
|
+
const binPath = (0, node_fs.realpathSync)((0, node_path.resolve)(packageRoot, relativeBin));
|
|
794
|
+
const childRelative = (0, node_path.relative)(packageRoot, binPath);
|
|
795
|
+
if (childRelative === "" || childRelative.startsWith("..") || (0, node_path.isAbsolute)(childRelative)) throw new Error(`${CHILD_PACKAGE} package bin resolves outside its package`);
|
|
796
|
+
return binPath;
|
|
797
|
+
}
|
|
798
|
+
function validateExactChildVersion(declared, installed) {
|
|
799
|
+
const declaredVersion = validateSemver(`${CHILD_PACKAGE} dependency`, declared);
|
|
800
|
+
const installedVersion = validateSemver(`${CHILD_PACKAGE} installed version`, installed);
|
|
801
|
+
if (declaredVersion !== installedVersion) throw new Error(`${CHILD_PACKAGE} installed version ${installedVersion} does not match exact dependency ${declaredVersion}`);
|
|
802
|
+
return declaredVersion;
|
|
803
|
+
}
|
|
804
|
+
function validateSemver(label, value) {
|
|
805
|
+
if (typeof value !== "string" || !/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/u.test(value)) throw new Error(`${label} is invalid`);
|
|
806
|
+
return value;
|
|
807
|
+
}
|
|
808
|
+
function jsonResult(data) {
|
|
809
|
+
return { content: [{
|
|
810
|
+
type: "text",
|
|
811
|
+
text: JSON.stringify(data)
|
|
812
|
+
}] };
|
|
813
|
+
}
|
|
814
|
+
function publicErrorResult(error) {
|
|
815
|
+
return errorResult(safeErrorMessage(error));
|
|
816
|
+
}
|
|
817
|
+
function errorResult(error, context = {}, secrets = []) {
|
|
818
|
+
return {
|
|
819
|
+
content: [{
|
|
820
|
+
type: "text",
|
|
821
|
+
text: JSON.stringify({
|
|
822
|
+
...context,
|
|
823
|
+
error: safeErrorMessage(error, secrets)
|
|
824
|
+
})
|
|
825
|
+
}],
|
|
826
|
+
isError: true
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
function log(message) {
|
|
830
|
+
process.stderr.write(`[xero-mcp-proxy] ${message}\n`);
|
|
831
|
+
}
|
|
832
|
+
if (isProcessEntrypoint(process.argv[1], require("url").pathToFileURL(__filename).href)) startServer().then((running) => {
|
|
833
|
+
let shutdownPromise;
|
|
834
|
+
const shutdown = () => {
|
|
835
|
+
if (shutdownPromise !== void 0) return;
|
|
836
|
+
shutdownPromise = running.close().catch((error) => {
|
|
837
|
+
log(`Failed to close cleanly: ${safeErrorMessage(error)}`);
|
|
838
|
+
}).then(() => {
|
|
839
|
+
process.exitCode = 0;
|
|
840
|
+
});
|
|
841
|
+
};
|
|
842
|
+
process.once("SIGTERM", shutdown);
|
|
843
|
+
process.once("SIGINT", shutdown);
|
|
844
|
+
}).catch((error) => {
|
|
845
|
+
log(`Fatal: ${safeErrorMessage(error)}`);
|
|
846
|
+
process.exitCode = 1;
|
|
847
|
+
});
|
|
848
|
+
//#endregion
|
|
849
|
+
exports.CHILD_BIN_PATH = CHILD_BIN_PATH;
|
|
850
|
+
exports.CHILD_PACKAGE_ROOT = CHILD_PACKAGE_ROOT;
|
|
851
|
+
exports.CHILD_VERSION = CHILD_VERSION;
|
|
852
|
+
exports.SERVER_VERSION = SERVER_VERSION;
|
|
853
|
+
exports.XeroRuntime = XeroRuntime;
|
|
854
|
+
exports.createProxyServer = createProxyServer;
|
|
855
|
+
exports.isProcessEntrypoint = isProcessEntrypoint;
|
|
856
|
+
exports.startServer = startServer;
|