@aihubspot/agent-trade-mcp 0.1.6 → 0.1.8

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 (3) hide show
  1. package/README.md +3 -3
  2. package/dist/index.js +258 -154
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -39,10 +39,10 @@ MCP clients that support the current protocol also receive the same envelope in
39
39
 
40
40
  Use these tools for requests that would otherwise return a broad market payload:
41
41
 
42
- - `market_search_symbols`: bounded symbol search and metadata.
42
+ - `market_search_symbols`: bounded symbol search and metadata. Use this for every MCP symbol lookup; it returns matching rows in `data.value.items`.
43
43
  - `market_get_ticker_summary`: watchlist, gainers, losers, and quote-volume leaders.
44
44
  - `market_get_depth_summary`: best bid/ask, spread, and up to 20 levels per side.
45
45
  - `market_get_trades_summary`: price range, buy/sell statistics, and up to 50 recent trades.
46
- - `market_get_klines_summary`: period change, high/low, latest candle, and up to 100 candles.
46
+ - `market_get_klines_summary`: period change, high/low, latest candle, and up to 100 candles. It defaults to `60min`; formal intervals are `1min`, `5min`, `15min`, `30min`, `60min`, `1day`, `1week`, and `1month`.
47
47
 
48
- The raw symbol, ticker, depth, trades, and kline tools remain available only when an exact full payload is explicitly needed. Raw trades and klines are capped at 100 rows by this client.
48
+ The complete raw symbols endpoint is intentionally CLI-only because its payload is too large for Agent contexts. Raw ticker, depth, trades, and kline tools remain available only when an exact payload is explicitly needed. Raw trades are capped at 100 rows; raw klines support the upstream maximum of 300 rows.
package/dist/index.js CHANGED
@@ -656,29 +656,82 @@ function optionalClientOrderId(input) {
656
656
  }
657
657
 
658
658
  // ../core/src/tools/asset-tools.ts
659
- var accountTypes = ["1", "2", "3", "4", "5"];
659
+ var ACCOUNT_TYPES = ["1", "2", "3", "4", "5"];
660
+ var ACCOUNT_TYPE_NAMES = {
661
+ "1": "Spot",
662
+ "2": "Isolated Margin",
663
+ "3": "Cross Margin",
664
+ "4": "C2C",
665
+ "5": "Derivatives"
666
+ };
667
+ var accountTypeSchema = {
668
+ type: "string",
669
+ oneOf: [
670
+ { const: "1", title: "Spot (1)" },
671
+ { const: "2", title: "Isolated Margin (2; symbol required)" },
672
+ { const: "3", title: "Cross Margin (3)" },
673
+ { const: "4", title: "C2C (4)" },
674
+ { const: "5", title: "Derivatives (5)" }
675
+ ],
676
+ description: "Account type: 1=Spot, 2=Isolated Margin (requires symbol), 3=Cross Margin, 4=C2C, 5=Derivatives. Do not use 2 for Derivatives."
677
+ };
678
+ var spotDerivativeAccounts = ["EXCHANGE", "FUTURE"];
660
679
  function accountType(value, name) {
661
680
  const result2 = requiredString(value, name);
662
- if (!accountTypes.includes(result2)) throw new AiHubError("AI_HUB_INVALID_ARGUMENT", `${name} must be an account type from 1 to 5.`);
681
+ if (!ACCOUNT_TYPES.includes(result2)) throw new AiHubError("AI_HUB_INVALID_ARGUMENT", `${name} must be an account type: 1=Spot, 2=Isolated Margin, 3=Cross Margin, 4=C2C, or 5=Derivatives.`);
663
682
  return result2;
664
683
  }
684
+ function accountTypeName(value) {
685
+ return ACCOUNT_TYPE_NAMES[value];
686
+ }
687
+ function isIsolatedMarginTransfer(fromAccountType, toAccountType) {
688
+ return fromAccountType === "2" || toAccountType === "2";
689
+ }
665
690
  function requiredTransfer(input) {
666
691
  const value = strictObject(input, ["fromAccountType", "toAccountType", "symbol", "coinSymbol", "amount"]);
667
- return { fromAccountType: accountType(value, "fromAccountType"), toAccountType: accountType(value, "toAccountType"), coinSymbol: requiredString(value, "coinSymbol"), amount: positiveDecimal(value, "amount"), ...optionalString(value, "symbol") ? { symbol: optionalString(value, "symbol") } : {} };
692
+ const fromAccountType = accountType(value, "fromAccountType");
693
+ const toAccountType = accountType(value, "toAccountType");
694
+ if (fromAccountType === toAccountType) throw new AiHubError("AI_HUB_INVALID_ARGUMENT", "fromAccountType and toAccountType must be different.");
695
+ const symbol = optionalString(value, "symbol");
696
+ if (isIsolatedMarginTransfer(fromAccountType, toAccountType) && !symbol) {
697
+ throw new AiHubError("AI_HUB_ISOLATED_MARGIN_SYMBOL_REQUIRED", "symbol is required when fromAccountType or toAccountType is 2 (Isolated Margin). Provide the isolated-margin trading pair, for example ETHUSDT.");
698
+ }
699
+ return { fromAccountType, toAccountType, coinSymbol: requiredString(value, "coinSymbol"), amount: positiveDecimal(value, "amount"), ...symbol ? { symbol } : {} };
668
700
  }
669
701
  function transferQuery(input) {
670
702
  const value = strictObject(input, ["fromAccountType", "toAccountType", "symbol", "coinSymbol", "page", "pageSize"]);
671
- return { fromAccountType: accountType(value, "fromAccountType"), toAccountType: accountType(value, "toAccountType"), page: optionalPositiveInteger(value, "page", 1), pageSize: optionalPositiveInteger(value, "pageSize", 20, 100), ...optionalString(value, "symbol") ? { symbol: optionalString(value, "symbol") } : {}, ...optionalString(value, "coinSymbol") ? { coinSymbol: optionalString(value, "coinSymbol") } : {} };
703
+ const fromAccountType = accountType(value, "fromAccountType");
704
+ const toAccountType = accountType(value, "toAccountType");
705
+ const symbol = optionalString(value, "symbol");
706
+ if (isIsolatedMarginTransfer(fromAccountType, toAccountType) && !symbol) {
707
+ throw new AiHubError("AI_HUB_ISOLATED_MARGIN_SYMBOL_REQUIRED", "symbol is required when fromAccountType or toAccountType is 2 (Isolated Margin). Provide the isolated-margin trading pair, for example ETHUSDT.");
708
+ }
709
+ return { fromAccountType, toAccountType, page: optionalPositiveInteger(value, "page", 1), pageSize: optionalPositiveInteger(value, "pageSize", 20, 100), ...symbol ? { symbol } : {}, ...optionalString(value, "coinSymbol") ? { coinSymbol: optionalString(value, "coinSymbol") } : {} };
672
710
  }
673
711
  function transferAccounts(input) {
674
712
  const value = strictObject(input, ["coinSymbol", "amount", "fromAccount", "toAccount"]);
675
- return { coinSymbol: requiredString(value, "coinSymbol"), amount: positiveDecimal(value, "amount"), fromAccount: requiredString(value, "fromAccount"), toAccount: requiredString(value, "toAccount") };
713
+ const fromAccount = requiredString(value, "fromAccount").toUpperCase();
714
+ const toAccount = requiredString(value, "toAccount").toUpperCase();
715
+ if (!spotDerivativeAccounts.includes(fromAccount) || !spotDerivativeAccounts.includes(toAccount) || fromAccount === toAccount) {
716
+ throw new AiHubError("AI_HUB_INVALID_ARGUMENT", "account transfer supports only EXCHANGE -> FUTURE or FUTURE -> EXCHANGE. Use wallet transfer for isolated margin, cross margin, or C2C.");
717
+ }
718
+ return { coinSymbol: requiredString(value, "coinSymbol"), amount: positiveDecimal(value, "amount"), fromAccount, toAccount };
676
719
  }
677
720
  function transferAccountHistory(input) {
678
721
  const value = strictObject(input, ["transferId", "coinSymbol", "fromAccount", "toAccount", "startTime", "endTime", "page", "limit"]);
679
722
  const transferId = optionalString(value, "transferId");
680
723
  if (!transferId && (!optionalString(value, "fromAccount") || !optionalString(value, "toAccount"))) throw new AiHubError("AI_HUB_INVALID_ARGUMENT", "fromAccount and toAccount are required when transferId is omitted.");
681
- return { ...transferId ? { transferId } : {}, ...optionalString(value, "coinSymbol") ? { coinSymbol: optionalString(value, "coinSymbol") } : {}, ...optionalString(value, "fromAccount") ? { fromAccount: optionalString(value, "fromAccount") } : {}, ...optionalString(value, "toAccount") ? { toAccount: optionalString(value, "toAccount") } : {}, ...optionalString(value, "startTime") ? { startTime: optionalString(value, "startTime") } : {}, ...optionalString(value, "endTime") ? { endTime: optionalString(value, "endTime") } : {}, page: optionalPositiveInteger(value, "page", 1), limit: optionalPositiveInteger(value, "limit", 20, 100) };
724
+ const fromAccount = optionalString(value, "fromAccount")?.toUpperCase();
725
+ const toAccount = optionalString(value, "toAccount")?.toUpperCase();
726
+ if (!transferId && (!spotDerivativeAccounts.includes(fromAccount) || !spotDerivativeAccounts.includes(toAccount) || fromAccount === toAccount)) {
727
+ throw new AiHubError("AI_HUB_INVALID_ARGUMENT", "account transfer history supports only EXCHANGE -> FUTURE or FUTURE -> EXCHANGE when transferId is omitted.");
728
+ }
729
+ return { ...transferId ? { transferId } : {}, ...optionalString(value, "coinSymbol") ? { coinSymbol: optionalString(value, "coinSymbol") } : {}, ...fromAccount ? { fromAccount } : {}, ...toAccount ? { toAccount } : {}, ...optionalString(value, "startTime") ? { startTime: optionalString(value, "startTime") } : {}, ...optionalString(value, "endTime") ? { endTime: optionalString(value, "endTime") } : {}, page: optionalPositiveInteger(value, "page", 1), limit: optionalPositiveInteger(value, "limit", 20, 100) };
730
+ }
731
+ function withAccountTypeMetadata(response, value) {
732
+ const metadata = { accountType: value, accountTypeName: accountTypeName(value) };
733
+ if (response && typeof response === "object" && !Array.isArray(response)) return { ...response, ...metadata };
734
+ return { ...metadata, response };
682
735
  }
683
736
  function pagedDates(input) {
684
737
  const value = strictObject(input, ["startTime", "endTime", "page", "pageSize"]);
@@ -691,18 +744,21 @@ function coinOnly(input) {
691
744
  var assetTools = [
692
745
  {
693
746
  name: "account_transfer",
694
- title: "Transfer Between Accounts",
695
- description: "Transfer an asset between documented account types after confirmation.",
747
+ title: "Transfer Between Spot and Derivatives",
748
+ description: "Transfer only between Spot and Derivatives after confirmation. Use fromAccount=EXCHANGE and toAccount=FUTURE for Spot to Derivatives; use the reverse for Derivatives to Spot. Do not pass numeric account types to this Tool.",
696
749
  cliPath: ["account", "transfer"],
697
750
  module: "spot-account",
698
751
  access: "signed",
699
752
  operation: "write",
700
753
  riskLevel: "high",
701
- inputSchema: { type: "object", properties: { coinSymbol: { type: "string" }, amount: { type: "string" }, fromAccount: { type: "string" }, toAccount: { type: "string" } }, required: ["coinSymbol", "amount", "fromAccount", "toAccount"], additionalProperties: false },
754
+ inputSchema: { type: "object", properties: { coinSymbol: { type: "string" }, amount: { type: "string" }, fromAccount: { type: "string", enum: spotDerivativeAccounts, description: "EXCHANGE means Spot; FUTURE means Derivatives." }, toAccount: { type: "string", enum: spotDerivativeAccounts, description: "EXCHANGE means Spot; FUTURE means Derivatives." } }, required: ["coinSymbol", "amount", "fromAccount", "toAccount"], additionalProperties: false },
702
755
  errorCodes: writeErrors,
703
756
  validate: transferAccounts,
704
757
  handler: (input, context) => context.api.signedPost("/sapi/v1/asset/transfer", input, signed(context)),
705
- writeSummary: (input) => ({ action: "account_transfer", ...input })
758
+ writeSummary: (input) => {
759
+ const value = input;
760
+ return { action: "spot_derivatives_transfer", coinSymbol: value.coinSymbol, amount: value.amount, fromAccount: value.fromAccount, fromAccountName: value.fromAccount === "EXCHANGE" ? "Spot" : "Derivatives", toAccount: value.toAccount, toAccountName: value.toAccount === "EXCHANGE" ? "Spot" : "Derivatives" };
761
+ }
706
762
  },
707
763
  {
708
764
  name: "account_get_transfer_history",
@@ -721,28 +777,31 @@ var assetTools = [
721
777
  {
722
778
  name: "wallet_universal_transfer",
723
779
  title: "Universal Asset Transfer",
724
- description: "Transfer between account types after confirmation.",
780
+ description: "Transfer between numeric account types after confirmation: 1=Spot, 2=Isolated Margin, 3=Cross Margin, 4=C2C, 5=Derivatives. Account type 2 is not Derivatives and requires symbol. For a straightforward Spot to Derivatives transfer, prefer account_transfer with EXCHANGE -> FUTURE.",
725
781
  cliPath: ["wallet", "transfer"],
726
782
  module: "spot-deposit-withdraw",
727
783
  access: "signed",
728
784
  operation: "write",
729
785
  riskLevel: "high",
730
- inputSchema: { type: "object", properties: { fromAccountType: { type: "string" }, toAccountType: { type: "string" }, symbol: { type: "string" }, coinSymbol: { type: "string" }, amount: { type: "string" } }, required: ["fromAccountType", "toAccountType", "coinSymbol", "amount"], additionalProperties: false },
786
+ inputSchema: { type: "object", properties: { fromAccountType: accountTypeSchema, toAccountType: accountTypeSchema, symbol: { type: "string", description: "Required isolated-margin trading pair when either account type is 2, for example ETHUSDT. Not a Derivatives identifier." }, coinSymbol: { type: "string" }, amount: { type: "string" } }, required: ["fromAccountType", "toAccountType", "coinSymbol", "amount"], additionalProperties: false },
731
787
  errorCodes: writeErrors,
732
788
  validate: requiredTransfer,
733
789
  handler: (input, context) => context.api.signedPost("/sapi/v1/asset/universal_transfer", input, signed(context)),
734
- writeSummary: (input) => ({ action: "universal_transfer", ...input })
790
+ writeSummary: (input) => {
791
+ const value = input;
792
+ return { action: "universal_transfer", fromAccountType: value.fromAccountType, fromAccountTypeName: accountTypeName(String(value.fromAccountType)), toAccountType: value.toAccountType, toAccountTypeName: accountTypeName(String(value.toAccountType)), coinSymbol: value.coinSymbol, amount: value.amount, symbol: value.symbol ?? null };
793
+ }
735
794
  },
736
795
  {
737
796
  name: "wallet_get_universal_transfer_history",
738
797
  title: "Get Universal Transfer History",
739
- description: "Query universal asset transfer history.",
798
+ description: "Query universal transfer history using account types 1=Spot, 2=Isolated Margin, 3=Cross Margin, 4=C2C, and 5=Derivatives.",
740
799
  cliPath: ["wallet", "transfer-history"],
741
800
  module: "spot-deposit-withdraw",
742
801
  access: "signed",
743
802
  operation: "read",
744
803
  riskLevel: "low",
745
- inputSchema: { type: "object", properties: { fromAccountType: { type: "string" }, toAccountType: { type: "string" }, symbol: { type: "string" }, coinSymbol: { type: "string" }, page: { type: "integer" }, pageSize: { type: "integer" } }, required: ["fromAccountType", "toAccountType"], additionalProperties: false },
804
+ inputSchema: { type: "object", properties: { fromAccountType: accountTypeSchema, toAccountType: accountTypeSchema, symbol: { type: "string", description: "Isolated-margin trading pair when account type 2 is involved." }, coinSymbol: { type: "string" }, page: { type: "integer" }, pageSize: { type: "integer" } }, required: ["fromAccountType", "toAccountType"], additionalProperties: false },
746
805
  errorCodes: signedReadErrors,
747
806
  validate: transferQuery,
748
807
  handler: (input, context) => context.api.signedPost("/sapi/v1/asset/universal_transfer_query", input, signed(context))
@@ -795,19 +854,22 @@ var assetTools = [
795
854
  {
796
855
  name: "wallet_get_transferable_assets",
797
856
  title: "Get Transferable Assets",
798
- description: "Query assets transferable from one account type.",
857
+ description: "Query transferable assets for one account type. The response echoes the selected account type and its name: 1=Spot, 2=Isolated Margin, 3=Cross Margin, 4=C2C, 5=Derivatives.",
799
858
  cliPath: ["wallet", "transferable-assets"],
800
859
  module: "spot-deposit-withdraw",
801
860
  access: "signed",
802
861
  operation: "read",
803
862
  riskLevel: "low",
804
- inputSchema: { type: "object", properties: { accountType: { type: "string" } }, required: ["accountType"], additionalProperties: false },
863
+ inputSchema: { type: "object", properties: { accountType: accountTypeSchema }, required: ["accountType"], additionalProperties: false },
805
864
  errorCodes: signedReadErrors,
806
865
  validate: (input) => {
807
866
  const value = strictObject(input, ["accountType"]);
808
867
  return { accountType: accountType(value, "accountType") };
809
868
  },
810
- handler: (input, context) => context.api.signedPost("/sapi/v1/asset/account/by_type", input, signed(context))
869
+ handler: async (input, context) => {
870
+ const value = input;
871
+ return withAccountTypeMetadata(await context.api.signedPost("/sapi/v1/asset/account/by_type", value, signed(context)), value.accountType);
872
+ }
811
873
  },
812
874
  {
813
875
  name: "wallet_get_exchange_account",
@@ -1106,8 +1168,164 @@ function summarizeKlines(value, symbol, interval) {
1106
1168
  };
1107
1169
  }
1108
1170
 
1171
+ // ../core/src/tools/symbol-rules.ts
1172
+ var CACHE_TTL_MS = 60 * 60 * 1e3;
1173
+ var cache = /* @__PURE__ */ new Map();
1174
+ var pendingLoads = /* @__PURE__ */ new Map();
1175
+ function cacheKey(context) {
1176
+ return `${context.profile.name}\0${context.profile.configVersion}\0${context.profile.openApiBaseUrl}`;
1177
+ }
1178
+ function normalizedSymbol(symbol) {
1179
+ return symbol.replaceAll("/", "").trim().toUpperCase();
1180
+ }
1181
+ function stringValue(value) {
1182
+ if (typeof value === "string" && value.trim()) return value.trim();
1183
+ if (typeof value === "number" && Number.isFinite(value)) return String(value);
1184
+ return void 0;
1185
+ }
1186
+ function nonNegativeInteger(value) {
1187
+ const result2 = typeof value === "number" ? value : typeof value === "string" ? Number(value) : Number.NaN;
1188
+ return Number.isInteger(result2) && result2 >= 0 ? result2 : void 0;
1189
+ }
1190
+ function rows(response) {
1191
+ if (Array.isArray(response)) return response.filter((item) => Boolean(item) && typeof item === "object" && !Array.isArray(item));
1192
+ if (!response || typeof response !== "object" || Array.isArray(response)) return [];
1193
+ const root = response;
1194
+ const candidates = [root.symbols, root.data, root.data?.symbols];
1195
+ for (const candidate of candidates) {
1196
+ if (Array.isArray(candidate)) return candidate.filter((item) => Boolean(item) && typeof item === "object" && !Array.isArray(item));
1197
+ }
1198
+ return [];
1199
+ }
1200
+ function parseRules(response) {
1201
+ const result2 = /* @__PURE__ */ new Map();
1202
+ for (const row of rows(response)) {
1203
+ const symbol = stringValue(row.symbol);
1204
+ if (!symbol) continue;
1205
+ result2.set(normalizedSymbol(symbol), {
1206
+ symbol,
1207
+ quantityPrecision: nonNegativeInteger(row.quantityPrecision),
1208
+ pricePrecision: nonNegativeInteger(row.pricePrecision),
1209
+ limitVolumeMin: stringValue(row.limitVolumeMin),
1210
+ limitAmountMin: stringValue(row.limitAmountMin),
1211
+ limitPriceMin: stringValue(row.limitPriceMin)
1212
+ });
1213
+ }
1214
+ return result2;
1215
+ }
1216
+ async function loadSnapshot(context) {
1217
+ const key = cacheKey(context);
1218
+ const current = cache.get(key);
1219
+ if (current && current.expiresAt > Date.now()) return current;
1220
+ const pending = pendingLoads.get(key);
1221
+ if (pending) return pending;
1222
+ const load = context.api.symbols().then((response) => {
1223
+ const entry = { response, rules: parseRules(response), expiresAt: Date.now() + CACHE_TTL_MS };
1224
+ cache.set(key, entry);
1225
+ return entry;
1226
+ }).finally(() => pendingLoads.delete(key));
1227
+ pendingLoads.set(key, load);
1228
+ return load;
1229
+ }
1230
+ async function getCachedSymbols(context) {
1231
+ return (await loadSnapshot(context)).response;
1232
+ }
1233
+ async function getSymbolRule(context, symbol) {
1234
+ const entry = await loadSnapshot(context);
1235
+ const rule = entry.rules.get(normalizedSymbol(symbol));
1236
+ if (!rule) throw new AiHubError("AI_HUB_SYMBOL_NOT_FOUND", `Symbol "${symbol}" was not returned by the configured tenant OpenAPI symbols endpoint.`);
1237
+ return rule;
1238
+ }
1239
+ function decimalScale(value) {
1240
+ return value.split(".")[1]?.length ?? 0;
1241
+ }
1242
+ function decimalParts(value) {
1243
+ const [whole, fraction = ""] = value.split(".");
1244
+ return { digits: BigInt(`${whole}${fraction}`.replace(/^0+(?=\d)/, "") || "0"), scale: fraction.length };
1245
+ }
1246
+ function compareDecimal(left, right) {
1247
+ const a = decimalParts(left);
1248
+ const b = decimalParts(right);
1249
+ const scale = Math.max(a.scale, b.scale);
1250
+ const leftValue = a.digits * 10n ** BigInt(scale - a.scale);
1251
+ const rightValue = b.digits * 10n ** BigInt(scale - b.scale);
1252
+ return leftValue === rightValue ? 0 : leftValue > rightValue ? 1 : -1;
1253
+ }
1254
+ function multipliedDecimal(left, right) {
1255
+ const a = decimalParts(left);
1256
+ const b = decimalParts(right);
1257
+ const scale = a.scale + b.scale;
1258
+ const digits = (a.digits * b.digits).toString();
1259
+ if (scale === 0) return digits;
1260
+ const padded = digits.padStart(scale + 1, "0");
1261
+ return `${padded.slice(0, -scale)}.${padded.slice(-scale)}`;
1262
+ }
1263
+ function assertPrecision(name, value, precision, symbol) {
1264
+ if (precision !== void 0 && decimalScale(value) > precision) {
1265
+ throw new AiHubError("AI_HUB_SYMBOL_PRECISION_INVALID", `${name} for ${symbol} allows at most ${precision} decimal places; received ${value}.`);
1266
+ }
1267
+ }
1268
+ function assertAtLeast(name, value, minimum, symbol) {
1269
+ if (minimum && compareDecimal(value, minimum) < 0) {
1270
+ throw new AiHubError("AI_HUB_SYMBOL_MINIMUM_NOT_MET", `${name} for ${symbol} must be at least ${minimum}; received ${value}.`);
1271
+ }
1272
+ }
1273
+ async function preflightSymbolOrder(context, order) {
1274
+ const rule = await getSymbolRule(context, order.symbol);
1275
+ if (order.type === "MARKET" && order.side === "BUY" && order.quoteAmount) {
1276
+ assertPrecision("quoteAmount", order.quoteAmount, rule.pricePrecision, rule.symbol);
1277
+ }
1278
+ if (order.baseQuantity) assertPrecision("baseQuantity", order.baseQuantity, rule.quantityPrecision, rule.symbol);
1279
+ if (order.price) assertPrecision("price", order.price, rule.pricePrecision, rule.symbol);
1280
+ if (order.type === "LIMIT" && order.baseQuantity && order.price) {
1281
+ assertAtLeast("baseQuantity", order.baseQuantity, rule.limitVolumeMin, rule.symbol);
1282
+ assertAtLeast("price", order.price, rule.limitPriceMin, rule.symbol);
1283
+ if (rule.limitAmountMin && compareDecimal(rule.limitAmountMin, "0") > 0) {
1284
+ assertAtLeast("limit order amount", multipliedDecimal(order.baseQuantity, order.price), rule.limitAmountMin, rule.symbol);
1285
+ }
1286
+ }
1287
+ return rule;
1288
+ }
1289
+
1109
1290
  // ../core/src/tools/market-tools.ts
1110
1291
  var readErrors = ["AI_HUB_INVALID_ARGUMENT", "AI_HUB_OPENAPI_NETWORK_ERROR", "AI_HUB_OPENAPI_HTTP_ERROR", "AI_HUB_OPENAPI_INVALID_RESPONSE", "AI_HUB_OPENAPI_BUSINESS_ERROR"];
1292
+ var KLINE_INTERVALS = ["1min", "5min", "15min", "30min", "60min", "1day", "1week", "1month"];
1293
+ var KLINE_INTERVAL_ALIASES = {
1294
+ "1m": "1min",
1295
+ "5m": "5min",
1296
+ "15m": "15min",
1297
+ "30m": "30min",
1298
+ "60m": "60min",
1299
+ "1h": "60min",
1300
+ "1d": "1day",
1301
+ "1w": "1week",
1302
+ "1mo": "1month",
1303
+ "1M": "1month"
1304
+ };
1305
+ var klineIntervalDescription = "Kline interval. Supported values: 1min, 5min, 15min, 30min, 60min, 1day, 1week, 1month. Use 60min, not 1h.";
1306
+ var klineTimezoneDescription = "Optional timezone such as UTC+08 or UTC-09. Defaults to UTC+08. The upstream API applies timezone-specific data only to periods above 60min.";
1307
+ function normalizeKlineInterval(value) {
1308
+ const trimmed = value.trim();
1309
+ const normalized = KLINE_INTERVAL_ALIASES[trimmed] ?? trimmed.toLowerCase();
1310
+ if (KLINE_INTERVALS.includes(normalized)) return normalized;
1311
+ throw new AiHubError("AI_HUB_INVALID_ARGUMENT", `interval must be one of: ${KLINE_INTERVALS.join(", ")}. Use 60min, not 1h.`);
1312
+ }
1313
+ function validateKlineInput(input, options) {
1314
+ const value = strictObject(input, ["symbol", "interval", "startTime", "endTime", "timezone", "limit"]);
1315
+ const startTime = value.startTime === void 0 ? void 0 : optionalInteger(value, "startTime", 0, 0, Number.MAX_SAFE_INTEGER);
1316
+ const endTime = value.endTime === void 0 ? void 0 : optionalInteger(value, "endTime", 0, 0, Number.MAX_SAFE_INTEGER);
1317
+ if (startTime !== void 0 && endTime !== void 0 && startTime > endTime) {
1318
+ throw new AiHubError("AI_HUB_INVALID_ARGUMENT", "startTime must be less than or equal to endTime.");
1319
+ }
1320
+ return {
1321
+ symbol: requiredString(value, "symbol"),
1322
+ interval: value.interval === void 0 && options.summary ? "60min" : normalizeKlineInterval(requiredString(value, "interval")),
1323
+ startTime,
1324
+ endTime,
1325
+ timezone: optionalString(value, "timezone"),
1326
+ limit: optionalInteger(value, "limit", options.summary ? 20 : 50, 1, options.summary ? 100 : 300)
1327
+ };
1328
+ }
1111
1329
  var marketTools = [
1112
1330
  {
1113
1331
  name: "market_ping",
@@ -1149,15 +1367,16 @@ var marketTools = [
1149
1367
  access: "public",
1150
1368
  operation: "read",
1151
1369
  riskLevel: "low",
1370
+ mcpVisible: false,
1152
1371
  inputSchema: { type: "object", additionalProperties: false },
1153
1372
  errorCodes: readErrors,
1154
1373
  validate: (input) => strictObject(input, []),
1155
- handler: (_input, context) => context.api.symbols()
1374
+ handler: (_input, context) => getCachedSymbols(context)
1156
1375
  },
1157
1376
  {
1158
1377
  name: "market_search_symbols",
1159
1378
  title: "Search Spot Symbols",
1160
- description: "Search the configured tenant's spot symbols and return a bounded metadata result. Use this instead of market_get_symbols unless complete raw metadata is explicitly required.",
1379
+ description: "Search the configured tenant's spot symbols and return a bounded metadata result. In MCP, matching rows are at data.value.items. Use this instead of market_get_symbols unless complete raw metadata is explicitly required.",
1161
1380
  cliPath: ["market", "symbols-search"],
1162
1381
  module: "spot-common",
1163
1382
  access: "public",
@@ -1171,7 +1390,7 @@ var marketTools = [
1171
1390
  },
1172
1391
  handler: async (input, context) => {
1173
1392
  const value = input;
1174
- return summarizeSymbols(await context.api.symbols(), value);
1393
+ return summarizeSymbols(await getCachedSymbols(context), value);
1175
1394
  }
1176
1395
  },
1177
1396
  {
@@ -1297,38 +1516,32 @@ var marketTools = [
1297
1516
  {
1298
1517
  name: "market_get_klines",
1299
1518
  title: "Get Spot Klines",
1300
- description: "Get spot candlestick data for one symbol and interval.",
1519
+ description: "Get raw spot candlestick data for one symbol and a supported interval. Use market_get_klines_summary for Agent analysis.",
1301
1520
  cliPath: ["market", "klines"],
1302
1521
  module: "spot-common",
1303
1522
  access: "public",
1304
1523
  operation: "read",
1305
1524
  riskLevel: "low",
1306
- inputSchema: { type: "object", properties: { symbol: { type: "string" }, interval: { type: "string" }, startTime: { type: "integer" }, endTime: { type: "integer" }, timezone: { type: "string" }, limit: { type: "integer", minimum: 1, maximum: 100 } }, required: ["symbol", "interval"], additionalProperties: false },
1525
+ 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 },
1307
1526
  errorCodes: readErrors,
1308
1527
  validate: (input) => {
1309
- const value = strictObject(input, ["symbol", "interval", "startTime", "endTime", "timezone", "limit"]);
1310
- const startTime = value.startTime === void 0 ? void 0 : optionalInteger(value, "startTime", 0, 0, Number.MAX_SAFE_INTEGER);
1311
- const endTime = value.endTime === void 0 ? void 0 : optionalInteger(value, "endTime", 0, 0, Number.MAX_SAFE_INTEGER);
1312
- return { symbol: requiredString(value, "symbol"), interval: requiredString(value, "interval"), startTime, endTime, timezone: optionalString(value, "timezone"), limit: optionalInteger(value, "limit", 50, 1, 100) };
1528
+ return validateKlineInput(input, { summary: false });
1313
1529
  },
1314
1530
  handler: (input, context) => context.api.klines(input)
1315
1531
  },
1316
1532
  {
1317
1533
  name: "market_get_klines_summary",
1318
1534
  title: "Get Spot Klines Summary",
1319
- description: "Get a bounded candle analysis with period change, high/low, latest candle, and a caller-limited candle sample. Prefer this to market_get_klines for Agent analysis.",
1535
+ description: "Get a bounded candle analysis with period change, high/low, latest candle, and a caller-limited candle sample. Defaults to 60min. Prefer this to market_get_klines for Agent analysis.",
1320
1536
  cliPath: ["market", "klines-summary"],
1321
1537
  module: "spot-common",
1322
1538
  access: "public",
1323
1539
  operation: "read",
1324
1540
  riskLevel: "low",
1325
- inputSchema: { type: "object", properties: { symbol: { type: "string" }, interval: { type: "string" }, startTime: { type: "integer" }, endTime: { type: "integer" }, timezone: { type: "string" }, limit: { type: "integer", minimum: 1, maximum: 100 } }, required: ["symbol", "interval"], additionalProperties: false },
1541
+ inputSchema: { type: "object", properties: { symbol: { type: "string", description: "Spot symbol, for example ETHUSDT or ETH/USDT." }, interval: { type: "string", enum: KLINE_INTERVALS, description: `${klineIntervalDescription} Defaults to 60min when omitted.` }, 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: 100, description: "Number of candles included in the bounded analysis." } }, required: ["symbol"], additionalProperties: false },
1326
1542
  errorCodes: readErrors,
1327
1543
  validate: (input) => {
1328
- const value = strictObject(input, ["symbol", "interval", "startTime", "endTime", "timezone", "limit"]);
1329
- const startTime = value.startTime === void 0 ? void 0 : optionalInteger(value, "startTime", 0, 0, Number.MAX_SAFE_INTEGER);
1330
- const endTime = value.endTime === void 0 ? void 0 : optionalInteger(value, "endTime", 0, 0, Number.MAX_SAFE_INTEGER);
1331
- return { symbol: requiredString(value, "symbol"), interval: requiredString(value, "interval"), startTime, endTime, timezone: optionalString(value, "timezone"), limit: optionalInteger(value, "limit", 20, 1, 100) };
1544
+ return validateKlineInput(input, { summary: true });
1332
1545
  },
1333
1546
  handler: async (input, context) => {
1334
1547
  const value = input;
@@ -1337,122 +1550,6 @@ var marketTools = [
1337
1550
  }
1338
1551
  ];
1339
1552
 
1340
- // ../core/src/tools/symbol-rules.ts
1341
- var CACHE_TTL_MS = 5 * 60 * 1e3;
1342
- var cache = /* @__PURE__ */ new Map();
1343
- var pendingLoads = /* @__PURE__ */ new Map();
1344
- function cacheKey(context) {
1345
- return `${context.profile.name}\0${context.profile.configVersion}\0${context.profile.openApiBaseUrl}`;
1346
- }
1347
- function normalizedSymbol(symbol) {
1348
- return symbol.replaceAll("/", "").trim().toUpperCase();
1349
- }
1350
- function stringValue(value) {
1351
- if (typeof value === "string" && value.trim()) return value.trim();
1352
- if (typeof value === "number" && Number.isFinite(value)) return String(value);
1353
- return void 0;
1354
- }
1355
- function nonNegativeInteger(value) {
1356
- const result2 = typeof value === "number" ? value : typeof value === "string" ? Number(value) : Number.NaN;
1357
- return Number.isInteger(result2) && result2 >= 0 ? result2 : void 0;
1358
- }
1359
- function rows(response) {
1360
- if (Array.isArray(response)) return response.filter((item) => Boolean(item) && typeof item === "object" && !Array.isArray(item));
1361
- if (!response || typeof response !== "object" || Array.isArray(response)) return [];
1362
- const root = response;
1363
- const candidates = [root.symbols, root.data, root.data?.symbols];
1364
- for (const candidate of candidates) {
1365
- if (Array.isArray(candidate)) return candidate.filter((item) => Boolean(item) && typeof item === "object" && !Array.isArray(item));
1366
- }
1367
- return [];
1368
- }
1369
- function parseRules(response) {
1370
- const result2 = /* @__PURE__ */ new Map();
1371
- for (const row of rows(response)) {
1372
- const symbol = stringValue(row.symbol);
1373
- if (!symbol) continue;
1374
- result2.set(normalizedSymbol(symbol), {
1375
- symbol,
1376
- quantityPrecision: nonNegativeInteger(row.quantityPrecision),
1377
- pricePrecision: nonNegativeInteger(row.pricePrecision),
1378
- limitVolumeMin: stringValue(row.limitVolumeMin),
1379
- limitAmountMin: stringValue(row.limitAmountMin),
1380
- limitPriceMin: stringValue(row.limitPriceMin)
1381
- });
1382
- }
1383
- return result2;
1384
- }
1385
- async function loadRules(context) {
1386
- const key = cacheKey(context);
1387
- const current = cache.get(key);
1388
- if (current && current.expiresAt > Date.now()) return current;
1389
- const pending = pendingLoads.get(key);
1390
- if (pending) return pending;
1391
- const load = context.api.symbols().then((response) => {
1392
- const entry = { rules: parseRules(response), expiresAt: Date.now() + CACHE_TTL_MS };
1393
- cache.set(key, entry);
1394
- return entry;
1395
- }).finally(() => pendingLoads.delete(key));
1396
- pendingLoads.set(key, load);
1397
- return load;
1398
- }
1399
- async function getSymbolRule(context, symbol) {
1400
- const entry = await loadRules(context);
1401
- const rule = entry.rules.get(normalizedSymbol(symbol));
1402
- if (!rule) throw new AiHubError("AI_HUB_SYMBOL_NOT_FOUND", `Symbol "${symbol}" was not returned by the configured tenant OpenAPI symbols endpoint.`);
1403
- return rule;
1404
- }
1405
- function decimalScale(value) {
1406
- return value.split(".")[1]?.length ?? 0;
1407
- }
1408
- function decimalParts(value) {
1409
- const [whole, fraction = ""] = value.split(".");
1410
- return { digits: BigInt(`${whole}${fraction}`.replace(/^0+(?=\d)/, "") || "0"), scale: fraction.length };
1411
- }
1412
- function compareDecimal(left, right) {
1413
- const a = decimalParts(left);
1414
- const b = decimalParts(right);
1415
- const scale = Math.max(a.scale, b.scale);
1416
- const leftValue = a.digits * 10n ** BigInt(scale - a.scale);
1417
- const rightValue = b.digits * 10n ** BigInt(scale - b.scale);
1418
- return leftValue === rightValue ? 0 : leftValue > rightValue ? 1 : -1;
1419
- }
1420
- function multipliedDecimal(left, right) {
1421
- const a = decimalParts(left);
1422
- const b = decimalParts(right);
1423
- const scale = a.scale + b.scale;
1424
- const digits = (a.digits * b.digits).toString();
1425
- if (scale === 0) return digits;
1426
- const padded = digits.padStart(scale + 1, "0");
1427
- return `${padded.slice(0, -scale)}.${padded.slice(-scale)}`;
1428
- }
1429
- function assertPrecision(name, value, precision, symbol) {
1430
- if (precision !== void 0 && decimalScale(value) > precision) {
1431
- throw new AiHubError("AI_HUB_SYMBOL_PRECISION_INVALID", `${name} for ${symbol} allows at most ${precision} decimal places; received ${value}.`);
1432
- }
1433
- }
1434
- function assertAtLeast(name, value, minimum, symbol) {
1435
- if (minimum && compareDecimal(value, minimum) < 0) {
1436
- throw new AiHubError("AI_HUB_SYMBOL_MINIMUM_NOT_MET", `${name} for ${symbol} must be at least ${minimum}; received ${value}.`);
1437
- }
1438
- }
1439
- async function preflightSymbolOrder(context, order) {
1440
- const rule = await getSymbolRule(context, order.symbol);
1441
- if (order.type === "MARKET" && order.side === "BUY" && order.quoteAmount) {
1442
- assertPrecision("quoteAmount", order.quoteAmount, rule.pricePrecision, rule.symbol);
1443
- }
1444
- if (order.baseQuantity) assertPrecision("baseQuantity", order.baseQuantity, rule.quantityPrecision, rule.symbol);
1445
- if (order.price) assertPrecision("price", order.price, rule.pricePrecision, rule.symbol);
1446
- if (order.type === "LIMIT" && order.baseQuantity && order.price) {
1447
- assertAtLeast("baseQuantity", order.baseQuantity, rule.limitVolumeMin, rule.symbol);
1448
- assertAtLeast("price", order.price, rule.limitPriceMin, rule.symbol);
1449
- if (rule.limitAmountMin && compareDecimal(rule.limitAmountMin, "0") > 0) {
1450
- assertAtLeast("limit order amount", multipliedDecimal(order.baseQuantity, order.price), rule.limitAmountMin, rule.symbol);
1451
- }
1452
- }
1453
- return rule;
1454
- }
1455
-
1456
1553
  // ../core/src/tools/margin-tools.ts
1457
1554
  function validateMarginSemanticOrder(input) {
1458
1555
  const value = strictObject(input, ["symbol", "side", "type", "quoteAmount", "baseQuantity", "price", "newClientOrderId", "isolated"]);
@@ -2526,6 +2623,9 @@ function toMcpTool(tool) {
2526
2623
  }
2527
2624
  };
2528
2625
  }
2626
+ function isMcpVisible(tool) {
2627
+ return tool.mcpVisible !== false;
2628
+ }
2529
2629
  function prepareToolName(tool) {
2530
2630
  const [prefix, ...rest] = tool.name.split("_");
2531
2631
  return `${prefix ?? "spot"}_prepare_${rest.join("_")}`;
@@ -2543,10 +2643,10 @@ function createServer(profileName, readOnly) {
2543
2643
  const registry = createToolRegistry();
2544
2644
  const writeExecutor = new ToolWriteExecutor(registry);
2545
2645
  const server = new Server(
2546
- { name: "ai-hub-agent-trade", version: "0.1.6" },
2646
+ { name: "ai-hub-agent-trade", version: "0.1.8" },
2547
2647
  {
2548
2648
  capabilities: { tools: {} },
2549
- 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 broad market questions, call market_search_symbols, market_get_ticker_summary, market_get_depth_summary, market_get_trades_summary, or market_get_klines_summary instead of fetching large raw payloads. 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."
2649
+ 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 symbol lookup, use market_search_symbols. It returns an object whose bounded matches are at data.value.items; 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."
2550
2650
  }
2551
2651
  );
2552
2652
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
@@ -2558,7 +2658,7 @@ function createServer(profileName, readOnly) {
2558
2658
  inputSchema: { type: "object", additionalProperties: false },
2559
2659
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
2560
2660
  },
2561
- ...registry.list({ readOnly }).flatMap((tool) => tool.operation === "write" ? [toPrepareMcpTool(tool)] : [toMcpTool(tool)]),
2661
+ ...registry.list({ readOnly }).filter(isMcpVisible).flatMap((tool) => tool.operation === "write" ? [toPrepareMcpTool(tool)] : [toMcpTool(tool)]),
2562
2662
  ...readOnly ? [] : [{
2563
2663
  name: CONFIRM_ACTION_TOOL,
2564
2664
  title: "Confirm Prepared Action",
@@ -2577,7 +2677,7 @@ function createServer(profileName, readOnly) {
2577
2677
  data: {
2578
2678
  profile: { name: context.profile.name, host: new URL(context.profile.openApiBaseUrl).host, configVersion: context.profile.configVersion },
2579
2679
  readOnly,
2580
- capabilities: registry.capabilities(context, { readOnly })
2680
+ capabilities: registry.capabilities(context, { readOnly }).filter((capability) => isMcpVisible(registry.byName(capability.name)))
2581
2681
  }
2582
2682
  });
2583
2683
  }
@@ -2588,10 +2688,14 @@ function createServer(profileName, readOnly) {
2588
2688
  }
2589
2689
  return result({ ok: true, data: await writeExecutor.confirm(input.confirmationId, input.userConfirmation, context) });
2590
2690
  }
2591
- const preparedTool = registry.list({ readOnly: false }).find((tool) => tool.operation === "write" && prepareToolName(tool) === request.params.name);
2691
+ const preparedTool = registry.list({ readOnly: false }).find((tool2) => tool2.operation === "write" && prepareToolName(tool2) === request.params.name);
2592
2692
  if (preparedTool) {
2593
2693
  return result({ ok: true, data: await writeExecutor.prepare(preparedTool.name, request.params.arguments ?? {}, context) });
2594
2694
  }
2695
+ const tool = registry.byName(request.params.name, { readOnly });
2696
+ if (!isMcpVisible(tool)) {
2697
+ throw new AiHubError("AI_HUB_TOOL_NOT_AVAILABLE", `Tool "${request.params.name}" is not available through MCP. Use market_search_symbols for symbol lookup.`);
2698
+ }
2595
2699
  const data = await registry.execute(request.params.name, request.params.arguments ?? {}, context, { readOnly });
2596
2700
  return toMcpReadResult(formatMcpData(data));
2597
2701
  } catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aihubspot/agent-trade-mcp",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "Local stdio MCP server for AI Hub spot trading tools",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {