@aihubspot/agent-trade-mcp 0.1.10 → 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 +201 -22
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -451,7 +451,7 @@ async function normalizeOpenApiBaseUrl(value) {
451
451
  throw new AiHubError("AI_HUB_UNSAFE_OPENAPI_URL", "Localhost is not allowed as an OpenAPI base URL.");
452
452
  }
453
453
  const records2 = isIP(url.hostname) ? [{ address: url.hostname }] : await lookup(url.hostname, { all: true, verbatim: true });
454
- if (records2.length === 0 || records2.some((record2) => blockedIp(record2.address))) {
454
+ if (records2.length === 0 || records2.some((record3) => blockedIp(record3.address))) {
455
455
  throw new AiHubError("AI_HUB_UNSAFE_OPENAPI_URL", "OpenAPI base URL must resolve only to public addresses.");
456
456
  }
457
457
  url.pathname = url.pathname.replace(/\/+$/, "");
@@ -581,16 +581,65 @@ function confirmationContext(context) {
581
581
  };
582
582
  }
583
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
+
584
633
  // ../core/src/tools/validation.ts
585
634
  function strictObject(input, allowedKeys) {
586
635
  if (!input || typeof input !== "object" || Array.isArray(input)) {
587
636
  throw new AiHubError("AI_HUB_INVALID_ARGUMENT", "Tool input must be an object.");
588
637
  }
589
- const record2 = input;
590
- for (const key of Object.keys(record2)) {
638
+ const record3 = input;
639
+ for (const key of Object.keys(record3)) {
591
640
  if (!allowedKeys.includes(key)) throw new AiHubError("AI_HUB_INVALID_ARGUMENT", `Unknown argument "${key}".`);
592
641
  }
593
- return record2;
642
+ return record3;
594
643
  }
595
644
  function requiredString(input, name) {
596
645
  const value = input[name];
@@ -630,6 +679,26 @@ var accountTools = [
630
679
  if (!context.credentials) throw new AiHubError("AI_HUB_CREDENTIAL_NOT_CONFIGURED", `Credentials are not configured for profile "${context.profile.name}".`);
631
680
  return context.api.account(context.credentials);
632
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
+ }
633
702
  }
634
703
  ];
635
704
 
@@ -945,7 +1014,7 @@ var assetTools = [
945
1014
  ];
946
1015
 
947
1016
  // ../core/src/tools/market-summaries.ts
948
- function record(value, message) {
1017
+ function record2(value, message) {
949
1018
  if (!value || typeof value !== "object" || Array.isArray(value)) {
950
1019
  throw new AiHubError("AI_HUB_OPENAPI_INVALID_RESPONSE", message);
951
1020
  }
@@ -1023,7 +1092,7 @@ function tickerItem(value) {
1023
1092
  }
1024
1093
  function tickerRows(value) {
1025
1094
  if (Array.isArray(value)) return records(value, "Ticker response must be an array.");
1026
- const payload = record(value, "Ticker response must be an array.");
1095
+ const payload = record2(value, "Ticker response must be an array.");
1027
1096
  if (Array.isArray(payload.items)) return records(payload.items, "Ticker response items must be an array.");
1028
1097
  throw new AiHubError("AI_HUB_OPENAPI_INVALID_RESPONSE", "Ticker response must be an array.");
1029
1098
  }
@@ -1052,7 +1121,7 @@ function normalizedSymbol(value) {
1052
1121
  return value.replaceAll("/", "").trim().toUpperCase();
1053
1122
  }
1054
1123
  function parsedSymbolRows(value) {
1055
- const payload = record(value, "Symbols response must be an object.");
1124
+ const payload = record2(value, "Symbols response must be an object.");
1056
1125
  const rows2 = records(payload.symbols, "Symbols response must contain a symbols array.");
1057
1126
  return rows2.map((item) => {
1058
1127
  const apiSymbol = text(item.symbol);
@@ -1159,7 +1228,7 @@ function level(value) {
1159
1228
  return { price: text(value[0]), quantity: text(value[1]) };
1160
1229
  }
1161
1230
  function summarizeDepth(value, symbol) {
1162
- const payload = record(value, "Depth response must be an object.");
1231
+ const payload = record2(value, "Depth response must be an object.");
1163
1232
  const bids = Array.isArray(payload.bids) ? payload.bids : [];
1164
1233
  const asks = Array.isArray(payload.asks) ? payload.asks : [];
1165
1234
  const bestBid = level(bids[0]);
@@ -1180,7 +1249,7 @@ function summarizeDepth(value, symbol) {
1180
1249
  };
1181
1250
  }
1182
1251
  function summarizeTrades(value, symbol) {
1183
- const payload = record(value, "Trades response must be an object.");
1252
+ const payload = record2(value, "Trades response must be an object.");
1184
1253
  const rows2 = records(payload.list, "Trades response must contain a list array.");
1185
1254
  let buyCount = 0;
1186
1255
  let sellCount = 0;
@@ -1325,6 +1394,8 @@ function parseRules(response) {
1325
1394
  if (!symbol) continue;
1326
1395
  result2.set(normalizedSymbol2(symbol), {
1327
1396
  symbol,
1397
+ baseAsset: stringValue(row.baseAssetName) ?? stringValue(row.baseAsset),
1398
+ quoteAsset: stringValue(row.quoteAssetName) ?? stringValue(row.quoteAsset),
1328
1399
  quantityPrecision: nonNegativeInteger(row.quantityPrecision),
1329
1400
  pricePrecision: nonNegativeInteger(row.pricePrecision),
1330
1401
  limitVolumeMin: stringValue(row.limitVolumeMin),
@@ -1368,7 +1439,7 @@ function decimalScale(value) {
1368
1439
  return value.split(".")[1]?.length ?? 0;
1369
1440
  }
1370
1441
  function decimalParts(value) {
1371
- const [whole, fraction = ""] = value.split(".");
1442
+ const [whole = "0", fraction = ""] = value.split(".");
1372
1443
  return { digits: BigInt(`${whole}${fraction}`.replace(/^0+(?=\d)/, "") || "0"), scale: fraction.length };
1373
1444
  }
1374
1445
  function compareDecimal(left, right) {
@@ -1379,6 +1450,31 @@ function compareDecimal(left, right) {
1379
1450
  const rightValue = b.digits * 10n ** BigInt(scale - b.scale);
1380
1451
  return leftValue === rightValue ? 0 : leftValue > rightValue ? 1 : -1;
1381
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
+ }
1382
1478
  function multipliedDecimal(left, right) {
1383
1479
  const a = decimalParts(left);
1384
1480
  const b = decimalParts(right);
@@ -1586,13 +1682,19 @@ var marketTools = [
1586
1682
  access: "public",
1587
1683
  operation: "read",
1588
1684
  riskLevel: "low",
1589
- 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
+ },
1590
1691
  errorCodes: readErrors,
1591
1692
  validate: (input) => {
1592
1693
  const value = strictObject(input, ["symbol", "symbols", "timeZone"]);
1593
1694
  const symbol = optionalString(value, "symbol");
1594
1695
  const symbols = optionalString(value, "symbols");
1595
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.");
1596
1698
  return { symbol, symbols, timeZone: optionalString(value, "timeZone") };
1597
1699
  },
1598
1700
  handler: (input, context) => context.api.ticker(input)
@@ -1626,6 +1728,7 @@ var marketTools = [
1626
1728
  access: "public",
1627
1729
  operation: "read",
1628
1730
  riskLevel: "low",
1731
+ mcpVisible: false,
1629
1732
  inputSchema: { type: "object", properties: { symbol: { type: "string" }, limit: { type: "integer", minimum: 1, maximum: 100 } }, required: ["symbol"], additionalProperties: false },
1630
1733
  errorCodes: readErrors,
1631
1734
  validate: (input) => {
@@ -1666,6 +1769,7 @@ var marketTools = [
1666
1769
  access: "public",
1667
1770
  operation: "read",
1668
1771
  riskLevel: "low",
1772
+ mcpVisible: false,
1669
1773
  inputSchema: { type: "object", properties: { symbol: { type: "string" }, limit: { type: "integer", minimum: 1, maximum: 100 } }, required: ["symbol"], additionalProperties: false },
1670
1774
  errorCodes: readErrors,
1671
1775
  validate: (input) => {
@@ -1706,6 +1810,7 @@ var marketTools = [
1706
1810
  access: "public",
1707
1811
  operation: "read",
1708
1812
  riskLevel: "low",
1813
+ mcpVisible: false,
1709
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 },
1710
1815
  errorCodes: readErrors,
1711
1816
  validate: (input) => {
@@ -1925,11 +2030,11 @@ var marginTools = [
1925
2030
  // ../core/src/tools/order-tools.ts
1926
2031
  import { randomUUID as randomUUID3 } from "crypto";
1927
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"];
1928
- 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"];
1929
- 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+)?$/;
1930
2035
  function positiveDecimal2(value, name) {
1931
2036
  const raw = requiredString(value, name);
1932
- if (!decimal.test(raw) || !/[1-9]/.test(raw.replace(".", ""))) {
2037
+ if (!decimal2.test(raw) || !/[1-9]/.test(raw.replace(".", ""))) {
1933
2038
  throw new AiHubError("AI_HUB_INVALID_ARGUMENT", `${name} must be a positive decimal string.`);
1934
2039
  }
1935
2040
  return raw;
@@ -2015,6 +2120,21 @@ function validateMarketSell(input) {
2015
2120
  const value = strictObject(input, ["symbol", "baseQuantity", "newClientOrderId", "recvWindow"]);
2016
2121
  return validateSemanticOrder({ ...value, side: "SELL", type: "MARKET" });
2017
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
+ }
2018
2138
  function validateLimitOrder(input) {
2019
2139
  const value = strictObject(input, ["symbol", "side", "baseQuantity", "price", "timeInForce", "newClientOrderId", "recvWindow"]);
2020
2140
  return validateSemanticOrder({ ...value, type: "LIMIT" });
@@ -2057,10 +2177,47 @@ function semanticOrderSummary(order) {
2057
2177
  newClientOrderId: order.newClientOrderId
2058
2178
  };
2059
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
+ }
2060
2191
  async function preflightSpotOrder(order, context) {
2061
2192
  await preflightSymbolOrder(context, order);
2062
2193
  return order;
2063
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
+ }
2064
2221
  async function preflightSpotBatch(input, context) {
2065
2222
  await Promise.all(input.orders.map((order) => preflightSymbolOrder(context, {
2066
2223
  symbol: input.symbol,
@@ -2206,6 +2363,22 @@ var orderTools = [
2206
2363
  handler: (input, context) => context.api.placeOrder(toOpenApiOrder(input), signed2(context)),
2207
2364
  writeSummary: (input) => semanticOrderSummary(input)
2208
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
+ },
2209
2382
  {
2210
2383
  name: "spot_limit_order",
2211
2384
  title: "Place Limit Spot Order",
@@ -2770,11 +2943,11 @@ var READ_RESULT_OUTPUT_SCHEMA = {
2770
2943
  required: ["ok", "data"]
2771
2944
  };
2772
2945
  function result(data) {
2773
- return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
2946
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
2774
2947
  }
2775
2948
  function toMcpReadResult(data) {
2776
2949
  const payload = { ok: true, data };
2777
- return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }], structuredContent: payload };
2950
+ return { content: [{ type: "text", text: JSON.stringify(payload) }], structuredContent: payload };
2778
2951
  }
2779
2952
  function formatMcpData(data) {
2780
2953
  if (Array.isArray(data)) {
@@ -2796,7 +2969,7 @@ function toMcpTool(tool) {
2796
2969
  return {
2797
2970
  name: tool.name,
2798
2971
  title: tool.title,
2799
- 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,
2800
2973
  inputSchema: tool.inputSchema,
2801
2974
  ...tool.operation === "read" ? { outputSchema: READ_RESULT_OUTPUT_SCHEMA } : {},
2802
2975
  annotations: {
@@ -2827,10 +3000,10 @@ function createServer(profileName, readOnly) {
2827
3000
  const registry = createToolRegistry();
2828
3001
  const writeExecutor = new ToolWriteExecutor(registry);
2829
3002
  const server = new Server(
2830
- { name: "ai-hub-agent-trade", version: "0.1.10" },
3003
+ { name: "ai-hub-agent-trade", version: "0.1.11" },
2831
3004
  {
2832
3005
  capabilities: { tools: {} },
2833
- 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."
2834
3007
  }
2835
3008
  );
2836
3009
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
@@ -2838,7 +3011,7 @@ function createServer(profileName, readOnly) {
2838
3011
  {
2839
3012
  name: CAPABILITIES_TOOL,
2840
3013
  title: "Server Capabilities Snapshot",
2841
- 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.",
2842
3015
  inputSchema: { type: "object", additionalProperties: false },
2843
3016
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
2844
3017
  },
@@ -2856,12 +3029,18 @@ function createServer(profileName, readOnly) {
2856
3029
  try {
2857
3030
  const context = await createToolExecutionContext(profileName);
2858
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
+ }, {});
2859
3037
  return result({
2860
3038
  ok: true,
2861
3039
  data: {
2862
3040
  profile: { name: context.profile.name, host: new URL(context.profile.openApiBaseUrl).host, configVersion: context.profile.configVersion },
2863
3041
  readOnly,
2864
- capabilities: registry.capabilities(context, { readOnly }).filter((capability) => isMcpVisible(registry.byName(capability.name)))
3042
+ serviceVersion: "0.1.11",
3043
+ toolCounts
2865
3044
  }
2866
3045
  });
2867
3046
  }
@@ -2878,7 +3057,7 @@ function createServer(profileName, readOnly) {
2878
3057
  }
2879
3058
  const tool = registry.byName(request.params.name, { readOnly });
2880
3059
  if (!isMcpVisible(tool)) {
2881
- 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.`);
2882
3061
  }
2883
3062
  const data = await registry.execute(request.params.name, request.params.arguments ?? {}, context, { readOnly });
2884
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.10",
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": {