@aihubspot/agent-trade-mcp 0.1.9 → 0.1.11
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/index.js +265 -70
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -56,6 +56,9 @@ function loadStoredCredentials(apiKey, secretKey) {
|
|
|
56
56
|
|
|
57
57
|
// ../core/src/confirmation.ts
|
|
58
58
|
import { createHash as createHash2, randomUUID } from "crypto";
|
|
59
|
+
import { chmod, mkdir, readFile, rename, rm, writeFile } from "fs/promises";
|
|
60
|
+
import { homedir } from "os";
|
|
61
|
+
import { join } from "path";
|
|
59
62
|
function stableJson(value) {
|
|
60
63
|
if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
|
|
61
64
|
if (value && typeof value === "object") {
|
|
@@ -66,6 +69,37 @@ function stableJson(value) {
|
|
|
66
69
|
function hash(input) {
|
|
67
70
|
return createHash2("sha256").update(stableJson({ action: input.action, payload: input.payload, context: input.context })).digest("hex");
|
|
68
71
|
}
|
|
72
|
+
function createPending(input, ttlMs, now) {
|
|
73
|
+
return {
|
|
74
|
+
...input,
|
|
75
|
+
confirmationId: randomUUID(),
|
|
76
|
+
requestHash: hash(input),
|
|
77
|
+
expiresAtMs: now() + ttlMs
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function preparedAction(pending) {
|
|
81
|
+
return {
|
|
82
|
+
confirmationId: pending.confirmationId,
|
|
83
|
+
expiresAt: new Date(pending.expiresAtMs).toISOString(),
|
|
84
|
+
requestHash: pending.requestHash,
|
|
85
|
+
action: pending.action,
|
|
86
|
+
summary: pending.summary,
|
|
87
|
+
requiresNewUserConfirmation: true,
|
|
88
|
+
nextStep: "Stop and wait for a new explicit user confirmation message. Do not confirm from the same user instruction that prepared this request."
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function confirmPending(pending, userConfirmation, context, now) {
|
|
92
|
+
if (typeof userConfirmation !== "string" || !userConfirmation.trim()) {
|
|
93
|
+
throw new AiHubError("AI_HUB_CONFIRMATION_REQUIRED", "A non-empty new explicit user confirmation message is required before a state-changing request can execute.");
|
|
94
|
+
}
|
|
95
|
+
if (pending.expiresAtMs < now()) {
|
|
96
|
+
throw new AiHubError("AI_HUB_CONFIRMATION_EXPIRED", "Confirmation has expired.");
|
|
97
|
+
}
|
|
98
|
+
if (stableJson(pending.context) !== stableJson(context)) {
|
|
99
|
+
throw new AiHubError("AI_HUB_CONFIRMATION_CONTEXT_CHANGED", "Profile, OpenAPI base URL, configuration, or credentials changed after prepare.");
|
|
100
|
+
}
|
|
101
|
+
return { action: pending.action, payload: pending.payload, requestHash: pending.requestHash };
|
|
102
|
+
}
|
|
69
103
|
var ConfirmationService = class {
|
|
70
104
|
constructor(ttlMs = 5 * 60 * 1e3, now = () => Date.now()) {
|
|
71
105
|
this.ttlMs = ttlMs;
|
|
@@ -75,19 +109,9 @@ var ConfirmationService = class {
|
|
|
75
109
|
now;
|
|
76
110
|
actions = /* @__PURE__ */ new Map();
|
|
77
111
|
prepare(input) {
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
this.actions.set(confirmationId, { ...input, confirmationId, requestHash, expiresAtMs, consumed: false });
|
|
82
|
-
return {
|
|
83
|
-
confirmationId,
|
|
84
|
-
expiresAt: new Date(expiresAtMs).toISOString(),
|
|
85
|
-
requestHash,
|
|
86
|
-
action: input.action,
|
|
87
|
-
summary: input.summary,
|
|
88
|
-
requiresNewUserConfirmation: true,
|
|
89
|
-
nextStep: "Stop and wait for a new explicit user confirmation message. Do not call confirm_action from the same user instruction that prepared this request."
|
|
90
|
-
};
|
|
112
|
+
const pending = createPending(input, this.ttlMs, this.now);
|
|
113
|
+
this.actions.set(pending.confirmationId, { ...pending, consumed: false });
|
|
114
|
+
return preparedAction(pending);
|
|
91
115
|
}
|
|
92
116
|
confirm(confirmationId, userConfirmation, context) {
|
|
93
117
|
if (typeof userConfirmation !== "string" || !userConfirmation.trim()) {
|
|
@@ -96,17 +120,9 @@ var ConfirmationService = class {
|
|
|
96
120
|
const pending = this.actions.get(confirmationId);
|
|
97
121
|
if (!pending) throw new AiHubError("AI_HUB_CONFIRMATION_NOT_FOUND", "Confirmation was not found or has already been consumed.");
|
|
98
122
|
if (pending.consumed) throw new AiHubError("AI_HUB_CONFIRMATION_CONSUMED", "Confirmation has already been consumed.");
|
|
99
|
-
if (pending.expiresAtMs < this.now()) {
|
|
100
|
-
this.actions.delete(confirmationId);
|
|
101
|
-
throw new AiHubError("AI_HUB_CONFIRMATION_EXPIRED", "Confirmation has expired.");
|
|
102
|
-
}
|
|
103
|
-
if (stableJson(pending.context) !== stableJson(context)) {
|
|
104
|
-
this.actions.delete(confirmationId);
|
|
105
|
-
throw new AiHubError("AI_HUB_CONFIRMATION_CONTEXT_CHANGED", "Profile, OpenAPI base URL, configuration, or credentials changed after prepare.");
|
|
106
|
-
}
|
|
107
123
|
pending.consumed = true;
|
|
108
124
|
this.actions.delete(confirmationId);
|
|
109
|
-
return
|
|
125
|
+
return confirmPending(pending, userConfirmation, context, this.now);
|
|
110
126
|
}
|
|
111
127
|
};
|
|
112
128
|
|
|
@@ -394,16 +410,16 @@ var AiHubSpotApi = class {
|
|
|
394
410
|
// ../core/src/config.ts
|
|
395
411
|
import { createHash as createHash3 } from "crypto";
|
|
396
412
|
import { lookup } from "dns/promises";
|
|
397
|
-
import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
398
|
-
import { homedir } from "os";
|
|
399
|
-
import { dirname, join } from "path";
|
|
413
|
+
import { chmod as chmod2, mkdir as mkdir2, readFile as readFile2, rename as rename2, writeFile as writeFile2 } from "fs/promises";
|
|
414
|
+
import { homedir as homedir2 } from "os";
|
|
415
|
+
import { dirname, join as join2 } from "path";
|
|
400
416
|
import { isIP } from "net";
|
|
401
417
|
import { parse, stringify } from "smol-toml";
|
|
402
418
|
var CONFIG_DIRECTORY = ".ai-hub";
|
|
403
419
|
var CONFIG_FILENAME = "config.toml";
|
|
404
420
|
var PROFILE_NAME = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/;
|
|
405
|
-
function configFilePath(home =
|
|
406
|
-
return
|
|
421
|
+
function configFilePath(home = homedir2()) {
|
|
422
|
+
return join2(home, CONFIG_DIRECTORY, CONFIG_FILENAME);
|
|
407
423
|
}
|
|
408
424
|
function validateProfileName(name) {
|
|
409
425
|
if (!PROFILE_NAME.test(name)) {
|
|
@@ -435,7 +451,7 @@ async function normalizeOpenApiBaseUrl(value) {
|
|
|
435
451
|
throw new AiHubError("AI_HUB_UNSAFE_OPENAPI_URL", "Localhost is not allowed as an OpenAPI base URL.");
|
|
436
452
|
}
|
|
437
453
|
const records2 = isIP(url.hostname) ? [{ address: url.hostname }] : await lookup(url.hostname, { all: true, verbatim: true });
|
|
438
|
-
if (records2.length === 0 || records2.some((
|
|
454
|
+
if (records2.length === 0 || records2.some((record3) => blockedIp(record3.address))) {
|
|
439
455
|
throw new AiHubError("AI_HUB_UNSAFE_OPENAPI_URL", "OpenAPI base URL must resolve only to public addresses.");
|
|
440
456
|
}
|
|
441
457
|
url.pathname = url.pathname.replace(/\/+$/, "");
|
|
@@ -467,7 +483,7 @@ var ConfigStore = class {
|
|
|
467
483
|
filePath;
|
|
468
484
|
async read() {
|
|
469
485
|
try {
|
|
470
|
-
return parseConfig(await
|
|
486
|
+
return parseConfig(await readFile2(this.filePath, "utf8"));
|
|
471
487
|
} catch (error) {
|
|
472
488
|
if (error.code === "ENOENT") return defaultConfig();
|
|
473
489
|
throw error;
|
|
@@ -538,13 +554,13 @@ var ConfigStore = class {
|
|
|
538
554
|
}
|
|
539
555
|
async write(config) {
|
|
540
556
|
const directory = dirname(this.filePath);
|
|
541
|
-
await
|
|
542
|
-
await
|
|
557
|
+
await mkdir2(directory, { recursive: true, mode: 448 });
|
|
558
|
+
await chmod2(directory, 448);
|
|
543
559
|
const temporaryPath = `${this.filePath}.${process.pid}.tmp`;
|
|
544
|
-
await
|
|
545
|
-
await
|
|
546
|
-
await
|
|
547
|
-
await
|
|
560
|
+
await writeFile2(temporaryPath, stringify(config), { mode: 384 });
|
|
561
|
+
await chmod2(temporaryPath, 384);
|
|
562
|
+
await rename2(temporaryPath, this.filePath);
|
|
563
|
+
await chmod2(this.filePath, 384);
|
|
548
564
|
}
|
|
549
565
|
};
|
|
550
566
|
|
|
@@ -565,16 +581,65 @@ function confirmationContext(context) {
|
|
|
565
581
|
};
|
|
566
582
|
}
|
|
567
583
|
|
|
584
|
+
// ../core/src/tools/account-balance.ts
|
|
585
|
+
function record(value) {
|
|
586
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
587
|
+
}
|
|
588
|
+
function decimal(value) {
|
|
589
|
+
if (typeof value === "string" && /^\d+(?:\.\d+)?$/.test(value.trim())) return value.trim();
|
|
590
|
+
if (typeof value === "number" && Number.isFinite(value) && value >= 0) return String(value);
|
|
591
|
+
return void 0;
|
|
592
|
+
}
|
|
593
|
+
function addDecimal(left, right) {
|
|
594
|
+
const [leftWhole, leftFraction = ""] = left.split(".");
|
|
595
|
+
const [rightWhole, rightFraction = ""] = right.split(".");
|
|
596
|
+
const scale = Math.max(leftFraction.length, rightFraction.length);
|
|
597
|
+
const leftValue = BigInt(`${leftWhole}${leftFraction.padEnd(scale, "0")}`);
|
|
598
|
+
const rightValue = BigInt(`${rightWhole}${rightFraction.padEnd(scale, "0")}`);
|
|
599
|
+
const digits = (leftValue + rightValue).toString().padStart(scale + 1, "0");
|
|
600
|
+
if (!scale) return digits;
|
|
601
|
+
const fraction = digits.slice(-scale).replace(/0+$/, "");
|
|
602
|
+
return fraction ? `${digits.slice(0, -scale)}.${fraction}` : digits.slice(0, -scale);
|
|
603
|
+
}
|
|
604
|
+
function balanceRows(payload) {
|
|
605
|
+
const root = record(payload);
|
|
606
|
+
const data = record(root?.data);
|
|
607
|
+
const candidates = [root?.balances, data?.balances, root?.data, data?.data];
|
|
608
|
+
for (const candidate of candidates) {
|
|
609
|
+
if (Array.isArray(candidate)) return candidate.filter((item) => Boolean(record(item)));
|
|
610
|
+
}
|
|
611
|
+
return [];
|
|
612
|
+
}
|
|
613
|
+
function findAssetBalance(payload, requestedAsset) {
|
|
614
|
+
const asset = requestedAsset.trim().toUpperCase();
|
|
615
|
+
const row = balanceRows(payload).find((item) => {
|
|
616
|
+
const candidate = item.asset ?? item.coinSymbol ?? item.currency ?? item.coin;
|
|
617
|
+
return typeof candidate === "string" && candidate.trim().toUpperCase() === asset;
|
|
618
|
+
});
|
|
619
|
+
if (!row) return { asset, available: "0", frozen: "0", total: "0", found: false };
|
|
620
|
+
const available = decimal(row.available ?? row.free ?? row.availableBalance ?? row.balance ?? row.total) ?? "0";
|
|
621
|
+
const frozen = decimal(row.frozen ?? row.locked ?? row.freeze ?? row.hold ?? row.lockedBalance) ?? "0";
|
|
622
|
+
const total = decimal(row.total ?? row.balance) ?? addDecimal(available, frozen);
|
|
623
|
+
return { asset, available, frozen, total, found: true };
|
|
624
|
+
}
|
|
625
|
+
function requireAvailableBalance(payload, asset) {
|
|
626
|
+
const balance = findAssetBalance(payload, asset);
|
|
627
|
+
if (!balance.found || balance.available === "0") {
|
|
628
|
+
throw new AiHubError("AI_HUB_INSUFFICIENT_AVAILABLE_BALANCE", `No available ${balance.asset} balance was found for this request.`);
|
|
629
|
+
}
|
|
630
|
+
return balance;
|
|
631
|
+
}
|
|
632
|
+
|
|
568
633
|
// ../core/src/tools/validation.ts
|
|
569
634
|
function strictObject(input, allowedKeys) {
|
|
570
635
|
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
571
636
|
throw new AiHubError("AI_HUB_INVALID_ARGUMENT", "Tool input must be an object.");
|
|
572
637
|
}
|
|
573
|
-
const
|
|
574
|
-
for (const key of Object.keys(
|
|
638
|
+
const record3 = input;
|
|
639
|
+
for (const key of Object.keys(record3)) {
|
|
575
640
|
if (!allowedKeys.includes(key)) throw new AiHubError("AI_HUB_INVALID_ARGUMENT", `Unknown argument "${key}".`);
|
|
576
641
|
}
|
|
577
|
-
return
|
|
642
|
+
return record3;
|
|
578
643
|
}
|
|
579
644
|
function requiredString(input, name) {
|
|
580
645
|
const value = input[name];
|
|
@@ -614,6 +679,26 @@ var accountTools = [
|
|
|
614
679
|
if (!context.credentials) throw new AiHubError("AI_HUB_CREDENTIAL_NOT_CONFIGURED", `Credentials are not configured for profile "${context.profile.name}".`);
|
|
615
680
|
return context.api.account(context.credentials);
|
|
616
681
|
}
|
|
682
|
+
},
|
|
683
|
+
{
|
|
684
|
+
name: "account_get_asset_balance",
|
|
685
|
+
title: "Get Asset Balance",
|
|
686
|
+
description: "Get one asset's available, frozen, and total spot balance. Use this instead of the full account overview when the user asks about one asset.",
|
|
687
|
+
cliPath: ["account", "asset-balance"],
|
|
688
|
+
module: "spot-account",
|
|
689
|
+
access: "signed",
|
|
690
|
+
operation: "read",
|
|
691
|
+
riskLevel: "low",
|
|
692
|
+
inputSchema: { type: "object", properties: { asset: { type: "string", minLength: 1, description: "Asset code, for example ETH or USDT." } }, required: ["asset"], additionalProperties: false },
|
|
693
|
+
errorCodes: ["AI_HUB_INVALID_ARGUMENT", "AI_HUB_CREDENTIAL_NOT_CONFIGURED", "AI_HUB_OPENAPI_NETWORK_ERROR", "AI_HUB_OPENAPI_HTTP_ERROR", "AI_HUB_OPENAPI_INVALID_RESPONSE", "AI_HUB_OPENAPI_BUSINESS_ERROR"],
|
|
694
|
+
validate: (input) => {
|
|
695
|
+
const value = strictObject(input, ["asset"]);
|
|
696
|
+
return { asset: requiredString(value, "asset").toUpperCase() };
|
|
697
|
+
},
|
|
698
|
+
handler: async (input, context) => {
|
|
699
|
+
if (!context.credentials) throw new AiHubError("AI_HUB_CREDENTIAL_NOT_CONFIGURED", `Credentials are not configured for profile "${context.profile.name}".`);
|
|
700
|
+
return findAssetBalance(await context.api.account(context.credentials), input.asset);
|
|
701
|
+
}
|
|
617
702
|
}
|
|
618
703
|
];
|
|
619
704
|
|
|
@@ -929,7 +1014,7 @@ var assetTools = [
|
|
|
929
1014
|
];
|
|
930
1015
|
|
|
931
1016
|
// ../core/src/tools/market-summaries.ts
|
|
932
|
-
function
|
|
1017
|
+
function record2(value, message) {
|
|
933
1018
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
934
1019
|
throw new AiHubError("AI_HUB_OPENAPI_INVALID_RESPONSE", message);
|
|
935
1020
|
}
|
|
@@ -1007,7 +1092,7 @@ function tickerItem(value) {
|
|
|
1007
1092
|
}
|
|
1008
1093
|
function tickerRows(value) {
|
|
1009
1094
|
if (Array.isArray(value)) return records(value, "Ticker response must be an array.");
|
|
1010
|
-
const payload =
|
|
1095
|
+
const payload = record2(value, "Ticker response must be an array.");
|
|
1011
1096
|
if (Array.isArray(payload.items)) return records(payload.items, "Ticker response items must be an array.");
|
|
1012
1097
|
throw new AiHubError("AI_HUB_OPENAPI_INVALID_RESPONSE", "Ticker response must be an array.");
|
|
1013
1098
|
}
|
|
@@ -1036,7 +1121,7 @@ function normalizedSymbol(value) {
|
|
|
1036
1121
|
return value.replaceAll("/", "").trim().toUpperCase();
|
|
1037
1122
|
}
|
|
1038
1123
|
function parsedSymbolRows(value) {
|
|
1039
|
-
const payload =
|
|
1124
|
+
const payload = record2(value, "Symbols response must be an object.");
|
|
1040
1125
|
const rows2 = records(payload.symbols, "Symbols response must contain a symbols array.");
|
|
1041
1126
|
return rows2.map((item) => {
|
|
1042
1127
|
const apiSymbol = text(item.symbol);
|
|
@@ -1143,7 +1228,7 @@ function level(value) {
|
|
|
1143
1228
|
return { price: text(value[0]), quantity: text(value[1]) };
|
|
1144
1229
|
}
|
|
1145
1230
|
function summarizeDepth(value, symbol) {
|
|
1146
|
-
const payload =
|
|
1231
|
+
const payload = record2(value, "Depth response must be an object.");
|
|
1147
1232
|
const bids = Array.isArray(payload.bids) ? payload.bids : [];
|
|
1148
1233
|
const asks = Array.isArray(payload.asks) ? payload.asks : [];
|
|
1149
1234
|
const bestBid = level(bids[0]);
|
|
@@ -1164,7 +1249,7 @@ function summarizeDepth(value, symbol) {
|
|
|
1164
1249
|
};
|
|
1165
1250
|
}
|
|
1166
1251
|
function summarizeTrades(value, symbol) {
|
|
1167
|
-
const payload =
|
|
1252
|
+
const payload = record2(value, "Trades response must be an object.");
|
|
1168
1253
|
const rows2 = records(payload.list, "Trades response must contain a list array.");
|
|
1169
1254
|
let buyCount = 0;
|
|
1170
1255
|
let sellCount = 0;
|
|
@@ -1235,9 +1320,9 @@ function summarizeKlines(value, symbol, interval) {
|
|
|
1235
1320
|
|
|
1236
1321
|
// ../core/src/tools/symbol-rules.ts
|
|
1237
1322
|
import { createHash as createHash4 } from "crypto";
|
|
1238
|
-
import { chmod as
|
|
1239
|
-
import { homedir as
|
|
1240
|
-
import { join as
|
|
1323
|
+
import { chmod as chmod3, mkdir as mkdir3, readFile as readFile3, rename as rename3, rm as rm2, writeFile as writeFile3 } from "fs/promises";
|
|
1324
|
+
import { homedir as homedir3 } from "os";
|
|
1325
|
+
import { join as join3 } from "path";
|
|
1241
1326
|
var CACHE_TTL_MS = 60 * 60 * 1e3;
|
|
1242
1327
|
var cache = /* @__PURE__ */ new Map();
|
|
1243
1328
|
var pendingLoads = /* @__PURE__ */ new Map();
|
|
@@ -1248,16 +1333,16 @@ function persistentCacheEnabled() {
|
|
|
1248
1333
|
return process.env.AI_HUB_DISABLE_PERSISTENT_CACHE !== "1";
|
|
1249
1334
|
}
|
|
1250
1335
|
function persistentCacheDirectory() {
|
|
1251
|
-
return process.env.AI_HUB_CACHE_DIR ??
|
|
1336
|
+
return process.env.AI_HUB_CACHE_DIR ?? join3(homedir3(), ".ai-hub", "cache");
|
|
1252
1337
|
}
|
|
1253
1338
|
function persistentCachePath(key) {
|
|
1254
1339
|
const hash2 = createHash4("sha256").update(key).digest("hex");
|
|
1255
|
-
return
|
|
1340
|
+
return join3(persistentCacheDirectory(), `symbols-${hash2}.json`);
|
|
1256
1341
|
}
|
|
1257
1342
|
async function loadPersistentSnapshot(key) {
|
|
1258
1343
|
if (!persistentCacheEnabled()) return void 0;
|
|
1259
1344
|
try {
|
|
1260
|
-
const parsed = JSON.parse(await
|
|
1345
|
+
const parsed = JSON.parse(await readFile3(persistentCachePath(key), "utf8"));
|
|
1261
1346
|
if (typeof parsed.expiresAt !== "number" || !Number.isFinite(parsed.expiresAt) || parsed.expiresAt <= Date.now() || !("response" in parsed)) return void 0;
|
|
1262
1347
|
return { expiresAt: parsed.expiresAt, response: parsed.response, rules: parseRules(parsed.response) };
|
|
1263
1348
|
} catch {
|
|
@@ -1270,14 +1355,14 @@ async function persistSnapshot(key, entry) {
|
|
|
1270
1355
|
const filePath = persistentCachePath(key);
|
|
1271
1356
|
const temporaryPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
1272
1357
|
try {
|
|
1273
|
-
await
|
|
1274
|
-
await
|
|
1275
|
-
await
|
|
1276
|
-
await
|
|
1277
|
-
await
|
|
1278
|
-
await
|
|
1358
|
+
await mkdir3(directory, { recursive: true, mode: 448 });
|
|
1359
|
+
await chmod3(directory, 448);
|
|
1360
|
+
await writeFile3(temporaryPath, JSON.stringify({ expiresAt: entry.expiresAt, response: entry.response }), { mode: 384 });
|
|
1361
|
+
await chmod3(temporaryPath, 384);
|
|
1362
|
+
await rename3(temporaryPath, filePath);
|
|
1363
|
+
await chmod3(filePath, 384);
|
|
1279
1364
|
} catch {
|
|
1280
|
-
await
|
|
1365
|
+
await rm2(temporaryPath, { force: true }).catch(() => void 0);
|
|
1281
1366
|
}
|
|
1282
1367
|
}
|
|
1283
1368
|
function normalizedSymbol2(symbol) {
|
|
@@ -1309,6 +1394,8 @@ function parseRules(response) {
|
|
|
1309
1394
|
if (!symbol) continue;
|
|
1310
1395
|
result2.set(normalizedSymbol2(symbol), {
|
|
1311
1396
|
symbol,
|
|
1397
|
+
baseAsset: stringValue(row.baseAssetName) ?? stringValue(row.baseAsset),
|
|
1398
|
+
quoteAsset: stringValue(row.quoteAssetName) ?? stringValue(row.quoteAsset),
|
|
1312
1399
|
quantityPrecision: nonNegativeInteger(row.quantityPrecision),
|
|
1313
1400
|
pricePrecision: nonNegativeInteger(row.pricePrecision),
|
|
1314
1401
|
limitVolumeMin: stringValue(row.limitVolumeMin),
|
|
@@ -1352,7 +1439,7 @@ function decimalScale(value) {
|
|
|
1352
1439
|
return value.split(".")[1]?.length ?? 0;
|
|
1353
1440
|
}
|
|
1354
1441
|
function decimalParts(value) {
|
|
1355
|
-
const [whole, fraction = ""] = value.split(".");
|
|
1442
|
+
const [whole = "0", fraction = ""] = value.split(".");
|
|
1356
1443
|
return { digits: BigInt(`${whole}${fraction}`.replace(/^0+(?=\d)/, "") || "0"), scale: fraction.length };
|
|
1357
1444
|
}
|
|
1358
1445
|
function compareDecimal(left, right) {
|
|
@@ -1363,6 +1450,31 @@ function compareDecimal(left, right) {
|
|
|
1363
1450
|
const rightValue = b.digits * 10n ** BigInt(scale - b.scale);
|
|
1364
1451
|
return leftValue === rightValue ? 0 : leftValue > rightValue ? 1 : -1;
|
|
1365
1452
|
}
|
|
1453
|
+
function floorDecimal(value, precision) {
|
|
1454
|
+
if (!/^\d+(?:\.\d+)?$/.test(value) || precision < 0 || !Number.isInteger(precision)) {
|
|
1455
|
+
throw new AiHubError("AI_HUB_OPENAPI_INVALID_RESPONSE", "A balance or symbol precision value could not be parsed.");
|
|
1456
|
+
}
|
|
1457
|
+
const [whole = "0", fraction = ""] = value.split(".");
|
|
1458
|
+
const kept = fraction.slice(0, precision);
|
|
1459
|
+
const normalizedWhole = whole.replace(/^0+(?=\d)/, "") || "0";
|
|
1460
|
+
const normalizedFraction = kept.replace(/0+$/, "");
|
|
1461
|
+
return normalizedFraction ? `${normalizedWhole}.${normalizedFraction}` : normalizedWhole;
|
|
1462
|
+
}
|
|
1463
|
+
function subtractNonNegativeDecimal(left, right) {
|
|
1464
|
+
const comparison = compareDecimal(left, right);
|
|
1465
|
+
if (comparison < 0) throw new AiHubError("AI_HUB_OPENAPI_INVALID_RESPONSE", "Balance arithmetic produced a negative result.");
|
|
1466
|
+
const a = decimalParts(left);
|
|
1467
|
+
const b = decimalParts(right);
|
|
1468
|
+
const scale = Math.max(a.scale, b.scale);
|
|
1469
|
+
const digits = a.digits * 10n ** BigInt(scale - a.scale) - b.digits * 10n ** BigInt(scale - b.scale);
|
|
1470
|
+
const raw = digits.toString().padStart(scale + 1, "0");
|
|
1471
|
+
if (!scale) return raw;
|
|
1472
|
+
const fraction = raw.slice(-scale).replace(/0+$/, "");
|
|
1473
|
+
return fraction ? `${raw.slice(0, -scale)}.${fraction}` : raw.slice(0, -scale);
|
|
1474
|
+
}
|
|
1475
|
+
function isAtLeastDecimal(value, minimum) {
|
|
1476
|
+
return compareDecimal(value, minimum) >= 0;
|
|
1477
|
+
}
|
|
1366
1478
|
function multipliedDecimal(left, right) {
|
|
1367
1479
|
const a = decimalParts(left);
|
|
1368
1480
|
const b = decimalParts(right);
|
|
@@ -1570,13 +1682,19 @@ var marketTools = [
|
|
|
1570
1682
|
access: "public",
|
|
1571
1683
|
operation: "read",
|
|
1572
1684
|
riskLevel: "low",
|
|
1573
|
-
inputSchema: {
|
|
1685
|
+
inputSchema: {
|
|
1686
|
+
type: "object",
|
|
1687
|
+
properties: { symbol: { type: "string", minLength: 1, description: "One exact symbol, for example ETHUSDT." }, symbols: { type: "string", minLength: 1, description: "An explicitly requested upstream symbol list." }, timeZone: { type: "string" } },
|
|
1688
|
+
oneOf: [{ required: ["symbol"] }, { required: ["symbols"] }],
|
|
1689
|
+
additionalProperties: false
|
|
1690
|
+
},
|
|
1574
1691
|
errorCodes: readErrors,
|
|
1575
1692
|
validate: (input) => {
|
|
1576
1693
|
const value = strictObject(input, ["symbol", "symbols", "timeZone"]);
|
|
1577
1694
|
const symbol = optionalString(value, "symbol");
|
|
1578
1695
|
const symbols = optionalString(value, "symbols");
|
|
1579
1696
|
if (!symbol && !symbols) throw new AiHubError("AI_HUB_INVALID_ARGUMENT", "market_get_ticker requires symbol or symbols. Use market_get_ticker_summary for all-market data.");
|
|
1697
|
+
if (symbol && symbols) throw new AiHubError("AI_HUB_INVALID_ARGUMENT", "market_get_ticker accepts either symbol or symbols, not both.");
|
|
1580
1698
|
return { symbol, symbols, timeZone: optionalString(value, "timeZone") };
|
|
1581
1699
|
},
|
|
1582
1700
|
handler: (input, context) => context.api.ticker(input)
|
|
@@ -1610,6 +1728,7 @@ var marketTools = [
|
|
|
1610
1728
|
access: "public",
|
|
1611
1729
|
operation: "read",
|
|
1612
1730
|
riskLevel: "low",
|
|
1731
|
+
mcpVisible: false,
|
|
1613
1732
|
inputSchema: { type: "object", properties: { symbol: { type: "string" }, limit: { type: "integer", minimum: 1, maximum: 100 } }, required: ["symbol"], additionalProperties: false },
|
|
1614
1733
|
errorCodes: readErrors,
|
|
1615
1734
|
validate: (input) => {
|
|
@@ -1650,6 +1769,7 @@ var marketTools = [
|
|
|
1650
1769
|
access: "public",
|
|
1651
1770
|
operation: "read",
|
|
1652
1771
|
riskLevel: "low",
|
|
1772
|
+
mcpVisible: false,
|
|
1653
1773
|
inputSchema: { type: "object", properties: { symbol: { type: "string" }, limit: { type: "integer", minimum: 1, maximum: 100 } }, required: ["symbol"], additionalProperties: false },
|
|
1654
1774
|
errorCodes: readErrors,
|
|
1655
1775
|
validate: (input) => {
|
|
@@ -1690,6 +1810,7 @@ var marketTools = [
|
|
|
1690
1810
|
access: "public",
|
|
1691
1811
|
operation: "read",
|
|
1692
1812
|
riskLevel: "low",
|
|
1813
|
+
mcpVisible: false,
|
|
1693
1814
|
inputSchema: { type: "object", properties: { symbol: { type: "string", description: "Spot symbol, for example ETHUSDT or ETH/USDT." }, interval: { type: "string", enum: KLINE_INTERVALS, description: klineIntervalDescription }, startTime: { type: "integer", description: "Optional inclusive Unix timestamp in milliseconds." }, endTime: { type: "integer", description: "Optional inclusive Unix timestamp in milliseconds." }, timezone: { type: "string", description: klineTimezoneDescription }, limit: { type: "integer", minimum: 1, maximum: 300, description: "Number of newest-first candles. The upstream maximum is 300." } }, required: ["symbol", "interval"], additionalProperties: false },
|
|
1694
1815
|
errorCodes: readErrors,
|
|
1695
1816
|
validate: (input) => {
|
|
@@ -1909,11 +2030,11 @@ var marginTools = [
|
|
|
1909
2030
|
// ../core/src/tools/order-tools.ts
|
|
1910
2031
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
1911
2032
|
var signedReadErrors2 = ["AI_HUB_INVALID_ARGUMENT", "AI_HUB_CREDENTIAL_NOT_CONFIGURED", "AI_HUB_OPENAPI_NETWORK_ERROR", "AI_HUB_OPENAPI_HTTP_ERROR", "AI_HUB_OPENAPI_INVALID_RESPONSE", "AI_HUB_OPENAPI_BUSINESS_ERROR"];
|
|
1912
|
-
var writeErrors2 = [...signedReadErrors2, "AI_HUB_WRITE_CONFIRMATION_REQUIRED", "AI_HUB_CONFIRMATION_REQUIRED", "AI_HUB_CONFIRMATION_EXPIRED", "AI_HUB_CONFIRMATION_CONTEXT_CHANGED", "AI_HUB_CONFIRMATION_NOT_FOUND"];
|
|
1913
|
-
var
|
|
2033
|
+
var writeErrors2 = [...signedReadErrors2, "AI_HUB_SYMBOL_NOT_FOUND", "AI_HUB_SYMBOL_PRECISION_INVALID", "AI_HUB_SYMBOL_MINIMUM_NOT_MET", "AI_HUB_INSUFFICIENT_AVAILABLE_BALANCE", "AI_HUB_WRITE_CONFIRMATION_REQUIRED", "AI_HUB_CONFIRMATION_REQUIRED", "AI_HUB_CONFIRMATION_EXPIRED", "AI_HUB_CONFIRMATION_CONTEXT_CHANGED", "AI_HUB_CONFIRMATION_NOT_FOUND"];
|
|
2034
|
+
var decimal2 = /^(?:0|[1-9]\d*)(?:\.\d+)?$/;
|
|
1914
2035
|
function positiveDecimal2(value, name) {
|
|
1915
2036
|
const raw = requiredString(value, name);
|
|
1916
|
-
if (!
|
|
2037
|
+
if (!decimal2.test(raw) || !/[1-9]/.test(raw.replace(".", ""))) {
|
|
1917
2038
|
throw new AiHubError("AI_HUB_INVALID_ARGUMENT", `${name} must be a positive decimal string.`);
|
|
1918
2039
|
}
|
|
1919
2040
|
return raw;
|
|
@@ -1999,6 +2120,21 @@ function validateMarketSell(input) {
|
|
|
1999
2120
|
const value = strictObject(input, ["symbol", "baseQuantity", "newClientOrderId", "recvWindow"]);
|
|
2000
2121
|
return validateSemanticOrder({ ...value, side: "SELL", type: "MARKET" });
|
|
2001
2122
|
}
|
|
2123
|
+
function validateSellAvailable(input) {
|
|
2124
|
+
const value = strictObject(input, ["symbol", "newClientOrderId", "recvWindow"]);
|
|
2125
|
+
const symbol = requiredString(value, "symbol");
|
|
2126
|
+
return {
|
|
2127
|
+
symbol,
|
|
2128
|
+
// Replaced by the signed balance and symbol-rule preflight before a preview is created.
|
|
2129
|
+
volume: "0",
|
|
2130
|
+
side: "SELL",
|
|
2131
|
+
type: "MARKET",
|
|
2132
|
+
intent: "market_sell",
|
|
2133
|
+
baseQuantity: "0",
|
|
2134
|
+
requestedMode: "available_balance",
|
|
2135
|
+
...optionalOrderFields(value)
|
|
2136
|
+
};
|
|
2137
|
+
}
|
|
2002
2138
|
function validateLimitOrder(input) {
|
|
2003
2139
|
const value = strictObject(input, ["symbol", "side", "baseQuantity", "price", "timeInForce", "newClientOrderId", "recvWindow"]);
|
|
2004
2140
|
return validateSemanticOrder({ ...value, type: "LIMIT" });
|
|
@@ -2041,10 +2177,47 @@ function semanticOrderSummary(order) {
|
|
|
2041
2177
|
newClientOrderId: order.newClientOrderId
|
|
2042
2178
|
};
|
|
2043
2179
|
}
|
|
2180
|
+
function sellAvailableSummary(order) {
|
|
2181
|
+
return {
|
|
2182
|
+
...semanticOrderSummary(order),
|
|
2183
|
+
requestedMode: order.requestedMode,
|
|
2184
|
+
baseAsset: order.baseAsset,
|
|
2185
|
+
availableBaseQuantity: order.availableBaseQuantity,
|
|
2186
|
+
executableBaseQuantity: order.executableBaseQuantity,
|
|
2187
|
+
remainderBaseQuantity: order.remainderBaseQuantity,
|
|
2188
|
+
rounding: "floored to the configured base-quantity precision"
|
|
2189
|
+
};
|
|
2190
|
+
}
|
|
2044
2191
|
async function preflightSpotOrder(order, context) {
|
|
2045
2192
|
await preflightSymbolOrder(context, order);
|
|
2046
2193
|
return order;
|
|
2047
2194
|
}
|
|
2195
|
+
async function preflightSellAvailable(order, context) {
|
|
2196
|
+
const rule = await getSymbolRule(context, order.symbol);
|
|
2197
|
+
if (!rule.baseAsset) {
|
|
2198
|
+
throw new AiHubError("AI_HUB_OPENAPI_INVALID_RESPONSE", `Symbol "${rule.symbol}" did not include a base asset.`);
|
|
2199
|
+
}
|
|
2200
|
+
const balance = requireAvailableBalance(await context.api.account(signed2(context)), rule.baseAsset);
|
|
2201
|
+
const executableBaseQuantity = floorDecimal(balance.available, rule.quantityPrecision ?? 0);
|
|
2202
|
+
if (executableBaseQuantity === "0") {
|
|
2203
|
+
throw new AiHubError("AI_HUB_INSUFFICIENT_AVAILABLE_BALANCE", `Available ${balance.asset} balance is below the executable quantity precision for ${rule.symbol}.`);
|
|
2204
|
+
}
|
|
2205
|
+
if (rule.limitVolumeMin && !isAtLeastDecimal(executableBaseQuantity, rule.limitVolumeMin)) {
|
|
2206
|
+
throw new AiHubError("AI_HUB_SYMBOL_MINIMUM_NOT_MET", `Available ${balance.asset} balance for ${rule.symbol} is below the minimum executable quantity of ${rule.limitVolumeMin}.`);
|
|
2207
|
+
}
|
|
2208
|
+
const prepared = {
|
|
2209
|
+
...order,
|
|
2210
|
+
symbol: rule.symbol,
|
|
2211
|
+
volume: executableBaseQuantity,
|
|
2212
|
+
baseQuantity: executableBaseQuantity,
|
|
2213
|
+
baseAsset: balance.asset,
|
|
2214
|
+
availableBaseQuantity: balance.available,
|
|
2215
|
+
executableBaseQuantity,
|
|
2216
|
+
remainderBaseQuantity: subtractNonNegativeDecimal(balance.available, executableBaseQuantity)
|
|
2217
|
+
};
|
|
2218
|
+
await preflightSymbolOrder(context, prepared);
|
|
2219
|
+
return prepared;
|
|
2220
|
+
}
|
|
2048
2221
|
async function preflightSpotBatch(input, context) {
|
|
2049
2222
|
await Promise.all(input.orders.map((order) => preflightSymbolOrder(context, {
|
|
2050
2223
|
symbol: input.symbol,
|
|
@@ -2190,6 +2363,22 @@ var orderTools = [
|
|
|
2190
2363
|
handler: (input, context) => context.api.placeOrder(toOpenApiOrder(input), signed2(context)),
|
|
2191
2364
|
writeSummary: (input) => semanticOrderSummary(input)
|
|
2192
2365
|
},
|
|
2366
|
+
{
|
|
2367
|
+
name: "spot_sell_available",
|
|
2368
|
+
title: "Prepare Market Sell of Available Balance",
|
|
2369
|
+
description: "Prepare a market SELL of the maximum available base-asset balance for one symbol. It reads the signed balance, floors only to the configured quantity precision, shows the executable amount and remainder, and still requires separate confirmation.",
|
|
2370
|
+
cliPath: ["spot", "order", "sell-available"],
|
|
2371
|
+
module: "spot-order",
|
|
2372
|
+
access: "signed",
|
|
2373
|
+
operation: "write",
|
|
2374
|
+
riskLevel: "high",
|
|
2375
|
+
inputSchema: { type: "object", properties: { symbol: { type: "string", description: "Spot symbol, for example ETHUSDT." }, newClientOrderId: { type: "string" }, recvWindow: { type: "string" } }, required: ["symbol"], additionalProperties: false },
|
|
2376
|
+
errorCodes: writeErrors2,
|
|
2377
|
+
validate: validateSellAvailable,
|
|
2378
|
+
preflight: preflightSellAvailable,
|
|
2379
|
+
handler: (input, context) => context.api.placeOrder(toOpenApiOrder(input), signed2(context)),
|
|
2380
|
+
writeSummary: (input) => sellAvailableSummary(input)
|
|
2381
|
+
},
|
|
2193
2382
|
{
|
|
2194
2383
|
name: "spot_limit_order",
|
|
2195
2384
|
title: "Place Limit Spot Order",
|
|
@@ -2511,7 +2700,7 @@ var ToolRegistry = class {
|
|
|
2511
2700
|
if (tool.operation !== "write" || !tool.writeSummary) throw new AiHubError("AI_HUB_TOOL_NOT_WRITE", `Tool "${name}" is not a confirmable write Tool.`);
|
|
2512
2701
|
const validInput = tool.validate(input);
|
|
2513
2702
|
const preflightInput = tool.preflight ? await tool.preflight(validInput, context) : validInput;
|
|
2514
|
-
return confirmations.prepare({
|
|
2703
|
+
return await confirmations.prepare({
|
|
2515
2704
|
action: tool.name,
|
|
2516
2705
|
payload: preflightInput,
|
|
2517
2706
|
context: confirmationContext(context),
|
|
@@ -2754,11 +2943,11 @@ var READ_RESULT_OUTPUT_SCHEMA = {
|
|
|
2754
2943
|
required: ["ok", "data"]
|
|
2755
2944
|
};
|
|
2756
2945
|
function result(data) {
|
|
2757
|
-
return { content: [{ type: "text", text: JSON.stringify(data
|
|
2946
|
+
return { content: [{ type: "text", text: JSON.stringify(data) }] };
|
|
2758
2947
|
}
|
|
2759
2948
|
function toMcpReadResult(data) {
|
|
2760
2949
|
const payload = { ok: true, data };
|
|
2761
|
-
return { content: [{ type: "text", text: JSON.stringify(payload
|
|
2950
|
+
return { content: [{ type: "text", text: JSON.stringify(payload) }], structuredContent: payload };
|
|
2762
2951
|
}
|
|
2763
2952
|
function formatMcpData(data) {
|
|
2764
2953
|
if (Array.isArray(data)) {
|
|
@@ -2780,7 +2969,7 @@ function toMcpTool(tool) {
|
|
|
2780
2969
|
return {
|
|
2781
2970
|
name: tool.name,
|
|
2782
2971
|
title: tool.title,
|
|
2783
|
-
description:
|
|
2972
|
+
description: tool.description,
|
|
2784
2973
|
inputSchema: tool.inputSchema,
|
|
2785
2974
|
...tool.operation === "read" ? { outputSchema: READ_RESULT_OUTPUT_SCHEMA } : {},
|
|
2786
2975
|
annotations: {
|
|
@@ -2811,10 +3000,10 @@ function createServer(profileName, readOnly) {
|
|
|
2811
3000
|
const registry = createToolRegistry();
|
|
2812
3001
|
const writeExecutor = new ToolWriteExecutor(registry);
|
|
2813
3002
|
const server = new Server(
|
|
2814
|
-
{ name: "ai-hub-agent-trade", version: "0.1.
|
|
3003
|
+
{ name: "ai-hub-agent-trade", version: "0.1.11" },
|
|
2815
3004
|
{
|
|
2816
3005
|
capabilities: { tools: {} },
|
|
2817
|
-
instructions: "
|
|
3006
|
+
instructions: "Read results use {ok:true,data}; inspect data.dataType before reading fields. Use bounded summary tools for broad market requests. For writes, call a prepare tool, show its preview, stop for a new user message, then call confirm_action with that message. MARKET BUY uses quoteAmount; MARKET SELL uses baseQuantity."
|
|
2818
3007
|
}
|
|
2819
3008
|
);
|
|
2820
3009
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
@@ -2822,7 +3011,7 @@ function createServer(profileName, readOnly) {
|
|
|
2822
3011
|
{
|
|
2823
3012
|
name: CAPABILITIES_TOOL,
|
|
2824
3013
|
title: "Server Capabilities Snapshot",
|
|
2825
|
-
description: "Return
|
|
3014
|
+
description: "Return a compact local server status. Tool schemas are available through the MCP tool list.",
|
|
2826
3015
|
inputSchema: { type: "object", additionalProperties: false },
|
|
2827
3016
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
|
|
2828
3017
|
},
|
|
@@ -2840,12 +3029,18 @@ function createServer(profileName, readOnly) {
|
|
|
2840
3029
|
try {
|
|
2841
3030
|
const context = await createToolExecutionContext(profileName);
|
|
2842
3031
|
if (request.params.name === CAPABILITIES_TOOL) {
|
|
3032
|
+
const visibleTools = registry.list({ readOnly }).filter(isMcpVisible);
|
|
3033
|
+
const toolCounts = visibleTools.reduce((counts, tool2) => {
|
|
3034
|
+
counts[tool2.operation] = (counts[tool2.operation] ?? 0) + 1;
|
|
3035
|
+
return counts;
|
|
3036
|
+
}, {});
|
|
2843
3037
|
return result({
|
|
2844
3038
|
ok: true,
|
|
2845
3039
|
data: {
|
|
2846
3040
|
profile: { name: context.profile.name, host: new URL(context.profile.openApiBaseUrl).host, configVersion: context.profile.configVersion },
|
|
2847
3041
|
readOnly,
|
|
2848
|
-
|
|
3042
|
+
serviceVersion: "0.1.11",
|
|
3043
|
+
toolCounts
|
|
2849
3044
|
}
|
|
2850
3045
|
});
|
|
2851
3046
|
}
|
|
@@ -2862,7 +3057,7 @@ function createServer(profileName, readOnly) {
|
|
|
2862
3057
|
}
|
|
2863
3058
|
const tool = registry.byName(request.params.name, { readOnly });
|
|
2864
3059
|
if (!isMcpVisible(tool)) {
|
|
2865
|
-
throw new AiHubError("AI_HUB_TOOL_NOT_AVAILABLE", `Tool "${request.params.name}" is not available through MCP. Use
|
|
3060
|
+
throw new AiHubError("AI_HUB_TOOL_NOT_AVAILABLE", `Tool "${request.params.name}" is not available through MCP. Use the corresponding bounded summary or symbol-browsing tool instead.`);
|
|
2866
3061
|
}
|
|
2867
3062
|
const data = await registry.execute(request.params.name, request.params.arguments ?? {}, context, { readOnly });
|
|
2868
3063
|
return toMcpReadResult(formatMcpData(data));
|