@aihubspot/agent-trade-mcp 0.1.6 → 0.1.7
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/README.md +3 -3
- package/dist/index.js +178 -136
- 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
|
|
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
|
@@ -1106,8 +1106,164 @@ function summarizeKlines(value, symbol, interval) {
|
|
|
1106
1106
|
};
|
|
1107
1107
|
}
|
|
1108
1108
|
|
|
1109
|
+
// ../core/src/tools/symbol-rules.ts
|
|
1110
|
+
var CACHE_TTL_MS = 60 * 60 * 1e3;
|
|
1111
|
+
var cache = /* @__PURE__ */ new Map();
|
|
1112
|
+
var pendingLoads = /* @__PURE__ */ new Map();
|
|
1113
|
+
function cacheKey(context) {
|
|
1114
|
+
return `${context.profile.name}\0${context.profile.configVersion}\0${context.profile.openApiBaseUrl}`;
|
|
1115
|
+
}
|
|
1116
|
+
function normalizedSymbol(symbol) {
|
|
1117
|
+
return symbol.replaceAll("/", "").trim().toUpperCase();
|
|
1118
|
+
}
|
|
1119
|
+
function stringValue(value) {
|
|
1120
|
+
if (typeof value === "string" && value.trim()) return value.trim();
|
|
1121
|
+
if (typeof value === "number" && Number.isFinite(value)) return String(value);
|
|
1122
|
+
return void 0;
|
|
1123
|
+
}
|
|
1124
|
+
function nonNegativeInteger(value) {
|
|
1125
|
+
const result2 = typeof value === "number" ? value : typeof value === "string" ? Number(value) : Number.NaN;
|
|
1126
|
+
return Number.isInteger(result2) && result2 >= 0 ? result2 : void 0;
|
|
1127
|
+
}
|
|
1128
|
+
function rows(response) {
|
|
1129
|
+
if (Array.isArray(response)) return response.filter((item) => Boolean(item) && typeof item === "object" && !Array.isArray(item));
|
|
1130
|
+
if (!response || typeof response !== "object" || Array.isArray(response)) return [];
|
|
1131
|
+
const root = response;
|
|
1132
|
+
const candidates = [root.symbols, root.data, root.data?.symbols];
|
|
1133
|
+
for (const candidate of candidates) {
|
|
1134
|
+
if (Array.isArray(candidate)) return candidate.filter((item) => Boolean(item) && typeof item === "object" && !Array.isArray(item));
|
|
1135
|
+
}
|
|
1136
|
+
return [];
|
|
1137
|
+
}
|
|
1138
|
+
function parseRules(response) {
|
|
1139
|
+
const result2 = /* @__PURE__ */ new Map();
|
|
1140
|
+
for (const row of rows(response)) {
|
|
1141
|
+
const symbol = stringValue(row.symbol);
|
|
1142
|
+
if (!symbol) continue;
|
|
1143
|
+
result2.set(normalizedSymbol(symbol), {
|
|
1144
|
+
symbol,
|
|
1145
|
+
quantityPrecision: nonNegativeInteger(row.quantityPrecision),
|
|
1146
|
+
pricePrecision: nonNegativeInteger(row.pricePrecision),
|
|
1147
|
+
limitVolumeMin: stringValue(row.limitVolumeMin),
|
|
1148
|
+
limitAmountMin: stringValue(row.limitAmountMin),
|
|
1149
|
+
limitPriceMin: stringValue(row.limitPriceMin)
|
|
1150
|
+
});
|
|
1151
|
+
}
|
|
1152
|
+
return result2;
|
|
1153
|
+
}
|
|
1154
|
+
async function loadSnapshot(context) {
|
|
1155
|
+
const key = cacheKey(context);
|
|
1156
|
+
const current = cache.get(key);
|
|
1157
|
+
if (current && current.expiresAt > Date.now()) return current;
|
|
1158
|
+
const pending = pendingLoads.get(key);
|
|
1159
|
+
if (pending) return pending;
|
|
1160
|
+
const load = context.api.symbols().then((response) => {
|
|
1161
|
+
const entry = { response, rules: parseRules(response), expiresAt: Date.now() + CACHE_TTL_MS };
|
|
1162
|
+
cache.set(key, entry);
|
|
1163
|
+
return entry;
|
|
1164
|
+
}).finally(() => pendingLoads.delete(key));
|
|
1165
|
+
pendingLoads.set(key, load);
|
|
1166
|
+
return load;
|
|
1167
|
+
}
|
|
1168
|
+
async function getCachedSymbols(context) {
|
|
1169
|
+
return (await loadSnapshot(context)).response;
|
|
1170
|
+
}
|
|
1171
|
+
async function getSymbolRule(context, symbol) {
|
|
1172
|
+
const entry = await loadSnapshot(context);
|
|
1173
|
+
const rule = entry.rules.get(normalizedSymbol(symbol));
|
|
1174
|
+
if (!rule) throw new AiHubError("AI_HUB_SYMBOL_NOT_FOUND", `Symbol "${symbol}" was not returned by the configured tenant OpenAPI symbols endpoint.`);
|
|
1175
|
+
return rule;
|
|
1176
|
+
}
|
|
1177
|
+
function decimalScale(value) {
|
|
1178
|
+
return value.split(".")[1]?.length ?? 0;
|
|
1179
|
+
}
|
|
1180
|
+
function decimalParts(value) {
|
|
1181
|
+
const [whole, fraction = ""] = value.split(".");
|
|
1182
|
+
return { digits: BigInt(`${whole}${fraction}`.replace(/^0+(?=\d)/, "") || "0"), scale: fraction.length };
|
|
1183
|
+
}
|
|
1184
|
+
function compareDecimal(left, right) {
|
|
1185
|
+
const a = decimalParts(left);
|
|
1186
|
+
const b = decimalParts(right);
|
|
1187
|
+
const scale = Math.max(a.scale, b.scale);
|
|
1188
|
+
const leftValue = a.digits * 10n ** BigInt(scale - a.scale);
|
|
1189
|
+
const rightValue = b.digits * 10n ** BigInt(scale - b.scale);
|
|
1190
|
+
return leftValue === rightValue ? 0 : leftValue > rightValue ? 1 : -1;
|
|
1191
|
+
}
|
|
1192
|
+
function multipliedDecimal(left, right) {
|
|
1193
|
+
const a = decimalParts(left);
|
|
1194
|
+
const b = decimalParts(right);
|
|
1195
|
+
const scale = a.scale + b.scale;
|
|
1196
|
+
const digits = (a.digits * b.digits).toString();
|
|
1197
|
+
if (scale === 0) return digits;
|
|
1198
|
+
const padded = digits.padStart(scale + 1, "0");
|
|
1199
|
+
return `${padded.slice(0, -scale)}.${padded.slice(-scale)}`;
|
|
1200
|
+
}
|
|
1201
|
+
function assertPrecision(name, value, precision, symbol) {
|
|
1202
|
+
if (precision !== void 0 && decimalScale(value) > precision) {
|
|
1203
|
+
throw new AiHubError("AI_HUB_SYMBOL_PRECISION_INVALID", `${name} for ${symbol} allows at most ${precision} decimal places; received ${value}.`);
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
function assertAtLeast(name, value, minimum, symbol) {
|
|
1207
|
+
if (minimum && compareDecimal(value, minimum) < 0) {
|
|
1208
|
+
throw new AiHubError("AI_HUB_SYMBOL_MINIMUM_NOT_MET", `${name} for ${symbol} must be at least ${minimum}; received ${value}.`);
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
async function preflightSymbolOrder(context, order) {
|
|
1212
|
+
const rule = await getSymbolRule(context, order.symbol);
|
|
1213
|
+
if (order.type === "MARKET" && order.side === "BUY" && order.quoteAmount) {
|
|
1214
|
+
assertPrecision("quoteAmount", order.quoteAmount, rule.pricePrecision, rule.symbol);
|
|
1215
|
+
}
|
|
1216
|
+
if (order.baseQuantity) assertPrecision("baseQuantity", order.baseQuantity, rule.quantityPrecision, rule.symbol);
|
|
1217
|
+
if (order.price) assertPrecision("price", order.price, rule.pricePrecision, rule.symbol);
|
|
1218
|
+
if (order.type === "LIMIT" && order.baseQuantity && order.price) {
|
|
1219
|
+
assertAtLeast("baseQuantity", order.baseQuantity, rule.limitVolumeMin, rule.symbol);
|
|
1220
|
+
assertAtLeast("price", order.price, rule.limitPriceMin, rule.symbol);
|
|
1221
|
+
if (rule.limitAmountMin && compareDecimal(rule.limitAmountMin, "0") > 0) {
|
|
1222
|
+
assertAtLeast("limit order amount", multipliedDecimal(order.baseQuantity, order.price), rule.limitAmountMin, rule.symbol);
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
return rule;
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1109
1228
|
// ../core/src/tools/market-tools.ts
|
|
1110
1229
|
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"];
|
|
1230
|
+
var KLINE_INTERVALS = ["1min", "5min", "15min", "30min", "60min", "1day", "1week", "1month"];
|
|
1231
|
+
var KLINE_INTERVAL_ALIASES = {
|
|
1232
|
+
"1m": "1min",
|
|
1233
|
+
"5m": "5min",
|
|
1234
|
+
"15m": "15min",
|
|
1235
|
+
"30m": "30min",
|
|
1236
|
+
"60m": "60min",
|
|
1237
|
+
"1h": "60min",
|
|
1238
|
+
"1d": "1day",
|
|
1239
|
+
"1w": "1week",
|
|
1240
|
+
"1mo": "1month",
|
|
1241
|
+
"1M": "1month"
|
|
1242
|
+
};
|
|
1243
|
+
var klineIntervalDescription = "Kline interval. Supported values: 1min, 5min, 15min, 30min, 60min, 1day, 1week, 1month. Use 60min, not 1h.";
|
|
1244
|
+
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.";
|
|
1245
|
+
function normalizeKlineInterval(value) {
|
|
1246
|
+
const trimmed = value.trim();
|
|
1247
|
+
const normalized = KLINE_INTERVAL_ALIASES[trimmed] ?? trimmed.toLowerCase();
|
|
1248
|
+
if (KLINE_INTERVALS.includes(normalized)) return normalized;
|
|
1249
|
+
throw new AiHubError("AI_HUB_INVALID_ARGUMENT", `interval must be one of: ${KLINE_INTERVALS.join(", ")}. Use 60min, not 1h.`);
|
|
1250
|
+
}
|
|
1251
|
+
function validateKlineInput(input, options) {
|
|
1252
|
+
const value = strictObject(input, ["symbol", "interval", "startTime", "endTime", "timezone", "limit"]);
|
|
1253
|
+
const startTime = value.startTime === void 0 ? void 0 : optionalInteger(value, "startTime", 0, 0, Number.MAX_SAFE_INTEGER);
|
|
1254
|
+
const endTime = value.endTime === void 0 ? void 0 : optionalInteger(value, "endTime", 0, 0, Number.MAX_SAFE_INTEGER);
|
|
1255
|
+
if (startTime !== void 0 && endTime !== void 0 && startTime > endTime) {
|
|
1256
|
+
throw new AiHubError("AI_HUB_INVALID_ARGUMENT", "startTime must be less than or equal to endTime.");
|
|
1257
|
+
}
|
|
1258
|
+
return {
|
|
1259
|
+
symbol: requiredString(value, "symbol"),
|
|
1260
|
+
interval: value.interval === void 0 && options.summary ? "60min" : normalizeKlineInterval(requiredString(value, "interval")),
|
|
1261
|
+
startTime,
|
|
1262
|
+
endTime,
|
|
1263
|
+
timezone: optionalString(value, "timezone"),
|
|
1264
|
+
limit: optionalInteger(value, "limit", options.summary ? 20 : 50, 1, options.summary ? 100 : 300)
|
|
1265
|
+
};
|
|
1266
|
+
}
|
|
1111
1267
|
var marketTools = [
|
|
1112
1268
|
{
|
|
1113
1269
|
name: "market_ping",
|
|
@@ -1149,15 +1305,16 @@ var marketTools = [
|
|
|
1149
1305
|
access: "public",
|
|
1150
1306
|
operation: "read",
|
|
1151
1307
|
riskLevel: "low",
|
|
1308
|
+
mcpVisible: false,
|
|
1152
1309
|
inputSchema: { type: "object", additionalProperties: false },
|
|
1153
1310
|
errorCodes: readErrors,
|
|
1154
1311
|
validate: (input) => strictObject(input, []),
|
|
1155
|
-
handler: (_input, context) => context
|
|
1312
|
+
handler: (_input, context) => getCachedSymbols(context)
|
|
1156
1313
|
},
|
|
1157
1314
|
{
|
|
1158
1315
|
name: "market_search_symbols",
|
|
1159
1316
|
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.",
|
|
1317
|
+
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
1318
|
cliPath: ["market", "symbols-search"],
|
|
1162
1319
|
module: "spot-common",
|
|
1163
1320
|
access: "public",
|
|
@@ -1171,7 +1328,7 @@ var marketTools = [
|
|
|
1171
1328
|
},
|
|
1172
1329
|
handler: async (input, context) => {
|
|
1173
1330
|
const value = input;
|
|
1174
|
-
return summarizeSymbols(await context
|
|
1331
|
+
return summarizeSymbols(await getCachedSymbols(context), value);
|
|
1175
1332
|
}
|
|
1176
1333
|
},
|
|
1177
1334
|
{
|
|
@@ -1297,38 +1454,32 @@ var marketTools = [
|
|
|
1297
1454
|
{
|
|
1298
1455
|
name: "market_get_klines",
|
|
1299
1456
|
title: "Get Spot Klines",
|
|
1300
|
-
description: "Get spot candlestick data for one symbol and interval.",
|
|
1457
|
+
description: "Get raw spot candlestick data for one symbol and a supported interval. Use market_get_klines_summary for Agent analysis.",
|
|
1301
1458
|
cliPath: ["market", "klines"],
|
|
1302
1459
|
module: "spot-common",
|
|
1303
1460
|
access: "public",
|
|
1304
1461
|
operation: "read",
|
|
1305
1462
|
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:
|
|
1463
|
+
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
1464
|
errorCodes: readErrors,
|
|
1308
1465
|
validate: (input) => {
|
|
1309
|
-
|
|
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) };
|
|
1466
|
+
return validateKlineInput(input, { summary: false });
|
|
1313
1467
|
},
|
|
1314
1468
|
handler: (input, context) => context.api.klines(input)
|
|
1315
1469
|
},
|
|
1316
1470
|
{
|
|
1317
1471
|
name: "market_get_klines_summary",
|
|
1318
1472
|
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.",
|
|
1473
|
+
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
1474
|
cliPath: ["market", "klines-summary"],
|
|
1321
1475
|
module: "spot-common",
|
|
1322
1476
|
access: "public",
|
|
1323
1477
|
operation: "read",
|
|
1324
1478
|
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"
|
|
1479
|
+
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
1480
|
errorCodes: readErrors,
|
|
1327
1481
|
validate: (input) => {
|
|
1328
|
-
|
|
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) };
|
|
1482
|
+
return validateKlineInput(input, { summary: true });
|
|
1332
1483
|
},
|
|
1333
1484
|
handler: async (input, context) => {
|
|
1334
1485
|
const value = input;
|
|
@@ -1337,122 +1488,6 @@ var marketTools = [
|
|
|
1337
1488
|
}
|
|
1338
1489
|
];
|
|
1339
1490
|
|
|
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
1491
|
// ../core/src/tools/margin-tools.ts
|
|
1457
1492
|
function validateMarginSemanticOrder(input) {
|
|
1458
1493
|
const value = strictObject(input, ["symbol", "side", "type", "quoteAmount", "baseQuantity", "price", "newClientOrderId", "isolated"]);
|
|
@@ -2526,6 +2561,9 @@ function toMcpTool(tool) {
|
|
|
2526
2561
|
}
|
|
2527
2562
|
};
|
|
2528
2563
|
}
|
|
2564
|
+
function isMcpVisible(tool) {
|
|
2565
|
+
return tool.mcpVisible !== false;
|
|
2566
|
+
}
|
|
2529
2567
|
function prepareToolName(tool) {
|
|
2530
2568
|
const [prefix, ...rest] = tool.name.split("_");
|
|
2531
2569
|
return `${prefix ?? "spot"}_prepare_${rest.join("_")}`;
|
|
@@ -2543,10 +2581,10 @@ function createServer(profileName, readOnly) {
|
|
|
2543
2581
|
const registry = createToolRegistry();
|
|
2544
2582
|
const writeExecutor = new ToolWriteExecutor(registry);
|
|
2545
2583
|
const server = new Server(
|
|
2546
|
-
{ name: "ai-hub-agent-trade", version: "0.1.
|
|
2584
|
+
{ name: "ai-hub-agent-trade", version: "0.1.7" },
|
|
2547
2585
|
{
|
|
2548
2586
|
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
|
|
2587
|
+
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. 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
2588
|
}
|
|
2551
2589
|
);
|
|
2552
2590
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
@@ -2558,7 +2596,7 @@ function createServer(profileName, readOnly) {
|
|
|
2558
2596
|
inputSchema: { type: "object", additionalProperties: false },
|
|
2559
2597
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
|
|
2560
2598
|
},
|
|
2561
|
-
...registry.list({ readOnly }).flatMap((tool) => tool.operation === "write" ? [toPrepareMcpTool(tool)] : [toMcpTool(tool)]),
|
|
2599
|
+
...registry.list({ readOnly }).filter(isMcpVisible).flatMap((tool) => tool.operation === "write" ? [toPrepareMcpTool(tool)] : [toMcpTool(tool)]),
|
|
2562
2600
|
...readOnly ? [] : [{
|
|
2563
2601
|
name: CONFIRM_ACTION_TOOL,
|
|
2564
2602
|
title: "Confirm Prepared Action",
|
|
@@ -2577,7 +2615,7 @@ function createServer(profileName, readOnly) {
|
|
|
2577
2615
|
data: {
|
|
2578
2616
|
profile: { name: context.profile.name, host: new URL(context.profile.openApiBaseUrl).host, configVersion: context.profile.configVersion },
|
|
2579
2617
|
readOnly,
|
|
2580
|
-
capabilities: registry.capabilities(context, { readOnly })
|
|
2618
|
+
capabilities: registry.capabilities(context, { readOnly }).filter((capability) => isMcpVisible(registry.byName(capability.name)))
|
|
2581
2619
|
}
|
|
2582
2620
|
});
|
|
2583
2621
|
}
|
|
@@ -2588,10 +2626,14 @@ function createServer(profileName, readOnly) {
|
|
|
2588
2626
|
}
|
|
2589
2627
|
return result({ ok: true, data: await writeExecutor.confirm(input.confirmationId, input.userConfirmation, context) });
|
|
2590
2628
|
}
|
|
2591
|
-
const preparedTool = registry.list({ readOnly: false }).find((
|
|
2629
|
+
const preparedTool = registry.list({ readOnly: false }).find((tool2) => tool2.operation === "write" && prepareToolName(tool2) === request.params.name);
|
|
2592
2630
|
if (preparedTool) {
|
|
2593
2631
|
return result({ ok: true, data: await writeExecutor.prepare(preparedTool.name, request.params.arguments ?? {}, context) });
|
|
2594
2632
|
}
|
|
2633
|
+
const tool = registry.byName(request.params.name, { readOnly });
|
|
2634
|
+
if (!isMcpVisible(tool)) {
|
|
2635
|
+
throw new AiHubError("AI_HUB_TOOL_NOT_AVAILABLE", `Tool "${request.params.name}" is not available through MCP. Use market_search_symbols for symbol lookup.`);
|
|
2636
|
+
}
|
|
2595
2637
|
const data = await registry.execute(request.params.name, request.params.arguments ?? {}, context, { readOnly });
|
|
2596
2638
|
return toMcpReadResult(formatMcpData(data));
|
|
2597
2639
|
} catch (error) {
|