@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.
- package/dist/index.js +201 -22
- 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((
|
|
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
|
|
590
|
-
for (const key of Object.keys(
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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: {
|
|
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
|
|
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 (!
|
|
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
|
|
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
|
|
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:
|
|
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.
|
|
3003
|
+
{ name: "ai-hub-agent-trade", version: "0.1.11" },
|
|
2831
3004
|
{
|
|
2832
3005
|
capabilities: { tools: {} },
|
|
2833
|
-
instructions: "
|
|
3006
|
+
instructions: "Read results use {ok:true,data}; inspect data.dataType before reading fields. Use bounded summary tools for broad market requests. For writes, call a prepare tool, show its preview, stop for a new user message, then call confirm_action with that message. MARKET BUY uses quoteAmount; MARKET SELL uses baseQuantity."
|
|
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
|
|
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
|
-
|
|
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
|
|
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));
|