@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.
Files changed (2) hide show
  1. package/dist/index.js +265 -70
  2. 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 expiresAtMs = this.now() + this.ttlMs;
79
- const confirmationId = randomUUID();
80
- const requestHash = hash(input);
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 { action: pending.action, payload: pending.payload, requestHash: pending.requestHash };
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 = homedir()) {
406
- return join(home, CONFIG_DIRECTORY, CONFIG_FILENAME);
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((record2) => blockedIp(record2.address))) {
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 readFile(this.filePath, "utf8"));
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 mkdir(directory, { recursive: true, mode: 448 });
542
- await chmod(directory, 448);
557
+ await mkdir2(directory, { recursive: true, mode: 448 });
558
+ await chmod2(directory, 448);
543
559
  const temporaryPath = `${this.filePath}.${process.pid}.tmp`;
544
- await writeFile(temporaryPath, stringify(config), { mode: 384 });
545
- await chmod(temporaryPath, 384);
546
- await rename(temporaryPath, this.filePath);
547
- await chmod(this.filePath, 384);
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 record2 = input;
574
- for (const key of Object.keys(record2)) {
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 record2;
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 record(value, message) {
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 = record(value, "Ticker response must be an array.");
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 = record(value, "Symbols response must be an object.");
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 = record(value, "Depth response must be an object.");
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 = record(value, "Trades response must be an object.");
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 chmod2, mkdir as mkdir2, readFile as readFile2, rename as rename2, rm, writeFile as writeFile2 } from "fs/promises";
1239
- import { homedir as homedir2 } from "os";
1240
- import { join as join2 } from "path";
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 ?? join2(homedir2(), ".ai-hub", "cache");
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 join2(persistentCacheDirectory(), `symbols-${hash2}.json`);
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 readFile2(persistentCachePath(key), "utf8"));
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 mkdir2(directory, { recursive: true, mode: 448 });
1274
- await chmod2(directory, 448);
1275
- await writeFile2(temporaryPath, JSON.stringify({ expiresAt: entry.expiresAt, response: entry.response }), { mode: 384 });
1276
- await chmod2(temporaryPath, 384);
1277
- await rename2(temporaryPath, filePath);
1278
- await chmod2(filePath, 384);
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 rm(temporaryPath, { force: true }).catch(() => void 0);
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: { type: "object", properties: { symbol: { type: "string" }, symbols: { type: "string" }, timeZone: { type: "string" } }, additionalProperties: false },
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 decimal = /^(?:0|[1-9]\d*)(?:\.\d+)?$/;
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 (!decimal.test(raw) || !/[1-9]/.test(raw.replace(".", ""))) {
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, null, 2) }] };
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, null, 2) }], structuredContent: 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: `${tool.description}${tool.operation === "read" ? " Successful MCP output is structured as { ok: true, data: ... }. For list results, use data.items and data.count; for object results, use data.value. Check data.dataType before formatting." : ""}`,
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.9" },
3003
+ { name: "ai-hub-agent-trade", version: "0.1.11" },
2815
3004
  {
2816
3005
  capabilities: { tools: {} },
2817
- instructions: "Every successful read-tool response provides both JSON text and MCP structuredContent in the envelope { ok: true, data: ... }. All read-tool data is normalized: dataType=array means use data.items and data.count; dataType=object or scalar means use data.value; dataType=null means no value. Inspect data.dataType before formatting and never assume undocumented nested keys. For a generic trading-pair list, call market_get_symbol_overview first; it returns only counts and a small sample. For paged or quote-asset browsing, use market_list_symbols. Use market_search_symbols only when the user gave a keyword; query is required. Use market_get_symbol_info only for exact precision and minimum rules. Do not use a symbol search as a generic all-symbol list. The complete raw symbols payload is intentionally unavailable in MCP. For broad market questions, call market_get_ticker_summary, market_get_depth_summary, market_get_trades_summary, or market_get_klines_summary instead of fetching large raw payloads. Account types are fixed: 1=Spot, 2=Isolated Margin, 3=Cross Margin, 4=C2C, 5=Derivatives. Never call account type 2 Derivatives. For Spot to Derivatives, use account_prepare_transfer with fromAccount=EXCHANGE and toAccount=FUTURE. Use wallet_prepare_universal_transfer for margin or C2C; account type 2 requires its isolated-margin trading pair in symbol. Prepare/confirm tools return their documented action payload directly in data. For every state-changing action, call only a spot_prepare_* or margin_prepare_* tool first and show its exact summary to the user. Stop and wait for a new, explicit user confirmation message. Only then call confirm_action with that new message verbatim in userConfirmation. Never call prepare and confirm consecutively for one user instruction; never infer confirmation from prior intent, silence, or an Agent-generated message. For spot and margin orders, MARKET BUY always uses quoteAmount (the quote asset to spend); MARKET SELL uses baseQuantity (the base asset to sell). Never reinterpret a requested base quantity as quoteAmount."
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 the available local profile and Tool Registry capability snapshot.",
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
- capabilities: registry.capabilities(context, { readOnly }).filter((capability) => isMcpVisible(registry.byName(capability.name)))
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 market_get_symbol_overview, market_list_symbols, market_search_symbols, or market_get_symbol_info instead.`);
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));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aihubspot/agent-trade-mcp",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
4
4
  "description": "Local stdio MCP server for AI Hub spot trading tools",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {