@aihubspot/agent-trade-mcp 0.1.8 → 0.1.9
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 +5 -2
- package/dist/index.js +211 -43
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -23,7 +23,7 @@ Setup registers the currently installed MCP binary through its absolute Node run
|
|
|
23
23
|
|
|
24
24
|
Use `spot_prepare_market_buy` or `margin_prepare_market_buy` only with `quoteAmount` (for example, the exact USDT amount to spend for `ETHUSDT`). Use `spot_prepare_market_sell` or `margin_prepare_market_sell` only with `baseQuantity` (the exact ETH amount to sell for `ETHUSDT`). A market buy cannot guarantee an exact base-asset quantity; it must never reinterpret a requested base quantity as a quote amount.
|
|
25
25
|
|
|
26
|
-
Before any order preview, MCP lazily loads `/sapi/v2/symbols` once per local profile and caches the symbol rules in memory
|
|
26
|
+
Before any order preview, MCP lazily loads `/sapi/v2/symbols` once per local profile and caches the symbol rules for one hour in memory and an isolated local cache. Known quantity/price precision and limit-order minimum violations are rejected before confirmation.
|
|
27
27
|
|
|
28
28
|
## Read response
|
|
29
29
|
|
|
@@ -39,7 +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
|
-
- `
|
|
42
|
+
- `market_get_symbol_overview`: counts by quote asset and a small sample. Use this first for a generic request to list trading pairs.
|
|
43
|
+
- `market_list_symbols`: a paged, optionally quote-asset-filtered list without order-rule metadata.
|
|
44
|
+
- `market_search_symbols`: required-keyword lookup with at most 20 basic matching rows. Do not use it for a generic pair list.
|
|
45
|
+
- `market_get_symbol_info`: exact precision and minimum order rules for one symbol only.
|
|
43
46
|
- `market_get_ticker_summary`: watchlist, gainers, losers, and quote-volume leaders.
|
|
44
47
|
- `market_get_depth_summary`: best bid/ask, spread, and up to 20 levels per side.
|
|
45
48
|
- `market_get_trades_summary`: price range, buy/sell statistics, and up to 50 recent trades.
|
package/dist/index.js
CHANGED
|
@@ -1032,47 +1032,112 @@ function summarizeTickers(value, options) {
|
|
|
1032
1032
|
topByQuoteVolume: [...rows2].sort(compareByNumericField("amount", "desc")).slice(0, limit).map(tickerItem)
|
|
1033
1033
|
};
|
|
1034
1034
|
}
|
|
1035
|
-
function
|
|
1035
|
+
function normalizedSymbol(value) {
|
|
1036
|
+
return value.replaceAll("/", "").trim().toUpperCase();
|
|
1037
|
+
}
|
|
1038
|
+
function parsedSymbolRows(value) {
|
|
1036
1039
|
const payload = record(value, "Symbols response must be an object.");
|
|
1037
1040
|
const rows2 = records(payload.symbols, "Symbols response must contain a symbols array.");
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1041
|
+
return rows2.map((item) => {
|
|
1042
|
+
const apiSymbol = text(item.symbol);
|
|
1043
|
+
const symbol = text(item.SymbolName) ?? apiSymbol ?? "";
|
|
1044
|
+
return {
|
|
1045
|
+
symbol,
|
|
1046
|
+
apiSymbol,
|
|
1047
|
+
baseAsset: text(item.baseAssetName) ?? text(item.baseAsset),
|
|
1048
|
+
quoteAsset: text(item.quoteAssetName) ?? text(item.quoteAsset),
|
|
1049
|
+
pricePrecision: item.pricePrecision ?? null,
|
|
1050
|
+
quantityPrecision: item.quantityPrecision ?? null,
|
|
1051
|
+
limitVolumeMin: text(item.limitVolumeMin),
|
|
1052
|
+
limitPriceMin: text(item.limitPriceMin),
|
|
1053
|
+
limitAmountMin: text(item.limitAmountMin)
|
|
1054
|
+
};
|
|
1055
|
+
}).filter((item) => Boolean(item.symbol));
|
|
1056
|
+
}
|
|
1057
|
+
function quoteAssetCounts(rows2) {
|
|
1058
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1059
|
+
for (const item of rows2) {
|
|
1060
|
+
const quote = item.quoteAsset?.toUpperCase();
|
|
1061
|
+
if (quote) counts.set(quote, (counts.get(quote) ?? 0) + 1);
|
|
1062
|
+
}
|
|
1063
|
+
return [...counts.entries()].sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0])).map(([asset, count]) => ({ asset, count }));
|
|
1064
|
+
}
|
|
1065
|
+
function basicSymbolItem(item) {
|
|
1066
|
+
return {
|
|
1067
|
+
symbol: item.symbol,
|
|
1068
|
+
apiSymbol: item.apiSymbol,
|
|
1069
|
+
baseAsset: item.baseAsset,
|
|
1070
|
+
quoteAsset: item.quoteAsset
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
1073
|
+
function fullSymbolItem(item) {
|
|
1074
|
+
return {
|
|
1075
|
+
...basicSymbolItem(item),
|
|
1076
|
+
pricePrecision: item.pricePrecision,
|
|
1077
|
+
quantityPrecision: item.quantityPrecision,
|
|
1078
|
+
limitVolumeMin: item.limitVolumeMin,
|
|
1079
|
+
limitPriceMin: item.limitPriceMin,
|
|
1080
|
+
limitAmountMin: item.limitAmountMin
|
|
1081
|
+
};
|
|
1082
|
+
}
|
|
1083
|
+
function sortSymbolMatches(rows2, query) {
|
|
1084
|
+
const normalizedQuery = query?.trim().toUpperCase();
|
|
1085
|
+
return [...rows2].sort((left, right) => {
|
|
1086
|
+
if (!normalizedQuery) return left.symbol.localeCompare(right.symbol);
|
|
1047
1087
|
const rank = (item) => {
|
|
1048
|
-
const symbol =
|
|
1049
|
-
return symbol.startsWith(`${
|
|
1088
|
+
const symbol = item.symbol.toUpperCase();
|
|
1089
|
+
return symbol.startsWith(`${normalizedQuery}/`) ? 0 : symbol.startsWith(normalizedQuery) ? 1 : 2;
|
|
1050
1090
|
};
|
|
1051
|
-
return rank(left) - rank(right);
|
|
1091
|
+
return rank(left) - rank(right) || left.symbol.localeCompare(right.symbol);
|
|
1052
1092
|
});
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
if (quote) quoteAssetCounts.set(quote, (quoteAssetCounts.get(quote) ?? 0) + 1);
|
|
1057
|
-
}
|
|
1058
|
-
const items = sortedMatches.slice(0, options.limit).map((item) => ({
|
|
1059
|
-
symbol: text(item.SymbolName) ?? text(item.symbol),
|
|
1060
|
-
apiSymbol: text(item.symbol),
|
|
1061
|
-
baseAsset: text(item.baseAssetName) ?? text(item.baseAsset),
|
|
1062
|
-
quoteAsset: text(item.quoteAssetName) ?? text(item.quoteAsset),
|
|
1063
|
-
pricePrecision: item.pricePrecision ?? null,
|
|
1064
|
-
quantityPrecision: item.quantityPrecision ?? null,
|
|
1065
|
-
limitVolumeMin: text(item.limitVolumeMin),
|
|
1066
|
-
limitPriceMin: text(item.limitPriceMin),
|
|
1067
|
-
limitAmountMin: text(item.limitAmountMin)
|
|
1068
|
-
}));
|
|
1093
|
+
}
|
|
1094
|
+
function summarizeSymbolOverview(value, options) {
|
|
1095
|
+
const rows2 = parsedSymbolRows(value);
|
|
1069
1096
|
return {
|
|
1070
1097
|
totalSymbols: rows2.length,
|
|
1098
|
+
quoteAssetCounts: quoteAssetCounts(rows2),
|
|
1099
|
+
sampleSymbols: sortSymbolMatches(rows2).slice(0, options.limit).map((item) => item.symbol)
|
|
1100
|
+
};
|
|
1101
|
+
}
|
|
1102
|
+
function listSymbols(value, options) {
|
|
1103
|
+
const allRows = parsedSymbolRows(value);
|
|
1104
|
+
const quoteAsset = options.quoteAsset?.trim().toUpperCase();
|
|
1105
|
+
const matches = sortSymbolMatches(allRows.filter((item) => !quoteAsset || item.quoteAsset?.toUpperCase() === quoteAsset));
|
|
1106
|
+
const items = matches.slice(options.offset, options.offset + options.limit).map(basicSymbolItem);
|
|
1107
|
+
const nextOffset = options.offset + items.length;
|
|
1108
|
+
return {
|
|
1109
|
+
totalSymbols: allRows.length,
|
|
1071
1110
|
matchedSymbols: matches.length,
|
|
1072
|
-
|
|
1111
|
+
quoteAsset: quoteAsset ?? null,
|
|
1112
|
+
offset: options.offset,
|
|
1113
|
+
limit: options.limit,
|
|
1114
|
+
nextOffset: nextOffset < matches.length ? nextOffset : null,
|
|
1073
1115
|
items
|
|
1074
1116
|
};
|
|
1075
1117
|
}
|
|
1118
|
+
function searchSymbols(value, options) {
|
|
1119
|
+
const allRows = parsedSymbolRows(value);
|
|
1120
|
+
const query = options.query.trim().toUpperCase();
|
|
1121
|
+
const quoteAsset = options.quoteAsset?.trim().toUpperCase();
|
|
1122
|
+
const matches = allRows.filter((item) => {
|
|
1123
|
+
const symbol = item.symbol.toUpperCase();
|
|
1124
|
+
const apiSymbol = item.apiSymbol?.toUpperCase() ?? "";
|
|
1125
|
+
return (symbol.includes(query) || apiSymbol.includes(query)) && (!quoteAsset || item.quoteAsset?.toUpperCase() === quoteAsset);
|
|
1126
|
+
});
|
|
1127
|
+
const sortedMatches = sortSymbolMatches(matches, query);
|
|
1128
|
+
return {
|
|
1129
|
+
matchedSymbols: matches.length,
|
|
1130
|
+
query,
|
|
1131
|
+
quoteAsset: quoteAsset ?? null,
|
|
1132
|
+
items: sortedMatches.slice(0, options.limit).map(basicSymbolItem)
|
|
1133
|
+
};
|
|
1134
|
+
}
|
|
1135
|
+
function getSymbolInfo(value, symbol) {
|
|
1136
|
+
const target = normalizedSymbol(symbol);
|
|
1137
|
+
const item = parsedSymbolRows(value).find((row) => normalizedSymbol(row.symbol) === target || row.apiSymbol !== null && normalizedSymbol(row.apiSymbol) === target);
|
|
1138
|
+
if (!item) throw new AiHubError("AI_HUB_SYMBOL_NOT_FOUND", `Symbol "${symbol}" was not returned by the configured tenant OpenAPI symbols endpoint.`);
|
|
1139
|
+
return fullSymbolItem(item);
|
|
1140
|
+
}
|
|
1076
1141
|
function level(value) {
|
|
1077
1142
|
if (!Array.isArray(value)) return { price: null, quantity: null };
|
|
1078
1143
|
return { price: text(value[0]), quantity: text(value[1]) };
|
|
@@ -1169,13 +1234,53 @@ function summarizeKlines(value, symbol, interval) {
|
|
|
1169
1234
|
}
|
|
1170
1235
|
|
|
1171
1236
|
// ../core/src/tools/symbol-rules.ts
|
|
1237
|
+
import { createHash as createHash4 } from "crypto";
|
|
1238
|
+
import { chmod as chmod2, mkdir as mkdir2, readFile as readFile2, rename as rename2, rm, writeFile as writeFile2 } from "fs/promises";
|
|
1239
|
+
import { homedir as homedir2 } from "os";
|
|
1240
|
+
import { join as join2 } from "path";
|
|
1172
1241
|
var CACHE_TTL_MS = 60 * 60 * 1e3;
|
|
1173
1242
|
var cache = /* @__PURE__ */ new Map();
|
|
1174
1243
|
var pendingLoads = /* @__PURE__ */ new Map();
|
|
1175
1244
|
function cacheKey(context) {
|
|
1176
1245
|
return `${context.profile.name}\0${context.profile.configVersion}\0${context.profile.openApiBaseUrl}`;
|
|
1177
1246
|
}
|
|
1178
|
-
function
|
|
1247
|
+
function persistentCacheEnabled() {
|
|
1248
|
+
return process.env.AI_HUB_DISABLE_PERSISTENT_CACHE !== "1";
|
|
1249
|
+
}
|
|
1250
|
+
function persistentCacheDirectory() {
|
|
1251
|
+
return process.env.AI_HUB_CACHE_DIR ?? join2(homedir2(), ".ai-hub", "cache");
|
|
1252
|
+
}
|
|
1253
|
+
function persistentCachePath(key) {
|
|
1254
|
+
const hash2 = createHash4("sha256").update(key).digest("hex");
|
|
1255
|
+
return join2(persistentCacheDirectory(), `symbols-${hash2}.json`);
|
|
1256
|
+
}
|
|
1257
|
+
async function loadPersistentSnapshot(key) {
|
|
1258
|
+
if (!persistentCacheEnabled()) return void 0;
|
|
1259
|
+
try {
|
|
1260
|
+
const parsed = JSON.parse(await readFile2(persistentCachePath(key), "utf8"));
|
|
1261
|
+
if (typeof parsed.expiresAt !== "number" || !Number.isFinite(parsed.expiresAt) || parsed.expiresAt <= Date.now() || !("response" in parsed)) return void 0;
|
|
1262
|
+
return { expiresAt: parsed.expiresAt, response: parsed.response, rules: parseRules(parsed.response) };
|
|
1263
|
+
} catch {
|
|
1264
|
+
return void 0;
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
async function persistSnapshot(key, entry) {
|
|
1268
|
+
if (!persistentCacheEnabled()) return;
|
|
1269
|
+
const directory = persistentCacheDirectory();
|
|
1270
|
+
const filePath = persistentCachePath(key);
|
|
1271
|
+
const temporaryPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
1272
|
+
try {
|
|
1273
|
+
await mkdir2(directory, { recursive: true, mode: 448 });
|
|
1274
|
+
await chmod2(directory, 448);
|
|
1275
|
+
await writeFile2(temporaryPath, JSON.stringify({ expiresAt: entry.expiresAt, response: entry.response }), { mode: 384 });
|
|
1276
|
+
await chmod2(temporaryPath, 384);
|
|
1277
|
+
await rename2(temporaryPath, filePath);
|
|
1278
|
+
await chmod2(filePath, 384);
|
|
1279
|
+
} catch {
|
|
1280
|
+
await rm(temporaryPath, { force: true }).catch(() => void 0);
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
function normalizedSymbol2(symbol) {
|
|
1179
1284
|
return symbol.replaceAll("/", "").trim().toUpperCase();
|
|
1180
1285
|
}
|
|
1181
1286
|
function stringValue(value) {
|
|
@@ -1202,7 +1307,7 @@ function parseRules(response) {
|
|
|
1202
1307
|
for (const row of rows(response)) {
|
|
1203
1308
|
const symbol = stringValue(row.symbol);
|
|
1204
1309
|
if (!symbol) continue;
|
|
1205
|
-
result2.set(
|
|
1310
|
+
result2.set(normalizedSymbol2(symbol), {
|
|
1206
1311
|
symbol,
|
|
1207
1312
|
quantityPrecision: nonNegativeInteger(row.quantityPrecision),
|
|
1208
1313
|
pricePrecision: nonNegativeInteger(row.pricePrecision),
|
|
@@ -1219,11 +1324,18 @@ async function loadSnapshot(context) {
|
|
|
1219
1324
|
if (current && current.expiresAt > Date.now()) return current;
|
|
1220
1325
|
const pending = pendingLoads.get(key);
|
|
1221
1326
|
if (pending) return pending;
|
|
1222
|
-
const load =
|
|
1327
|
+
const load = (async () => {
|
|
1328
|
+
const persisted = await loadPersistentSnapshot(key);
|
|
1329
|
+
if (persisted) {
|
|
1330
|
+
cache.set(key, persisted);
|
|
1331
|
+
return persisted;
|
|
1332
|
+
}
|
|
1333
|
+
const response = await context.api.symbols();
|
|
1223
1334
|
const entry = { response, rules: parseRules(response), expiresAt: Date.now() + CACHE_TTL_MS };
|
|
1224
1335
|
cache.set(key, entry);
|
|
1336
|
+
await persistSnapshot(key, entry);
|
|
1225
1337
|
return entry;
|
|
1226
|
-
}).finally(() => pendingLoads.delete(key));
|
|
1338
|
+
})().finally(() => pendingLoads.delete(key));
|
|
1227
1339
|
pendingLoads.set(key, load);
|
|
1228
1340
|
return load;
|
|
1229
1341
|
}
|
|
@@ -1232,7 +1344,7 @@ async function getCachedSymbols(context) {
|
|
|
1232
1344
|
}
|
|
1233
1345
|
async function getSymbolRule(context, symbol) {
|
|
1234
1346
|
const entry = await loadSnapshot(context);
|
|
1235
|
-
const rule = entry.rules.get(
|
|
1347
|
+
const rule = entry.rules.get(normalizedSymbol2(symbol));
|
|
1236
1348
|
if (!rule) throw new AiHubError("AI_HUB_SYMBOL_NOT_FOUND", `Symbol "${symbol}" was not returned by the configured tenant OpenAPI symbols endpoint.`);
|
|
1237
1349
|
return rule;
|
|
1238
1350
|
}
|
|
@@ -1289,6 +1401,7 @@ async function preflightSymbolOrder(context, order) {
|
|
|
1289
1401
|
|
|
1290
1402
|
// ../core/src/tools/market-tools.ts
|
|
1291
1403
|
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"];
|
|
1404
|
+
var symbolReadErrors = [...readErrors, "AI_HUB_SYMBOL_NOT_FOUND"];
|
|
1292
1405
|
var KLINE_INTERVALS = ["1min", "5min", "15min", "30min", "60min", "1day", "1week", "1month"];
|
|
1293
1406
|
var KLINE_INTERVAL_ALIASES = {
|
|
1294
1407
|
"1m": "1min",
|
|
@@ -1361,7 +1474,7 @@ var marketTools = [
|
|
|
1361
1474
|
{
|
|
1362
1475
|
name: "market_get_symbols",
|
|
1363
1476
|
title: "Get Spot Symbols",
|
|
1364
|
-
description: "Get complete spot symbol metadata from the configured tenant OpenAPI. Use market_search_symbols for
|
|
1477
|
+
description: "Get complete spot symbol metadata from the configured tenant OpenAPI. Use market_get_symbol_overview, market_list_symbols, market_search_symbols, or market_get_symbol_info for Agent-facing symbol requests, because this response may be large.",
|
|
1365
1478
|
cliPath: ["market", "symbols"],
|
|
1366
1479
|
module: "spot-common",
|
|
1367
1480
|
access: "public",
|
|
@@ -1373,26 +1486,81 @@ var marketTools = [
|
|
|
1373
1486
|
validate: (input) => strictObject(input, []),
|
|
1374
1487
|
handler: (_input, context) => getCachedSymbols(context)
|
|
1375
1488
|
},
|
|
1489
|
+
{
|
|
1490
|
+
name: "market_get_symbol_overview",
|
|
1491
|
+
title: "Get Spot Symbol Overview",
|
|
1492
|
+
description: "Get a small overview of all configured spot symbols: total count, quote-asset counts, and a bounded sample of display symbols. Use this for generic requests such as listing available trading pairs.",
|
|
1493
|
+
cliPath: ["market", "symbols-overview"],
|
|
1494
|
+
module: "spot-common",
|
|
1495
|
+
access: "public",
|
|
1496
|
+
operation: "read",
|
|
1497
|
+
riskLevel: "low",
|
|
1498
|
+
inputSchema: { type: "object", properties: { limit: { type: "integer", minimum: 1, maximum: 20, description: "Number of sample display symbols. Defaults to 12." } }, additionalProperties: false },
|
|
1499
|
+
errorCodes: readErrors,
|
|
1500
|
+
validate: (input) => {
|
|
1501
|
+
const value = strictObject(input, ["limit"]);
|
|
1502
|
+
return { limit: optionalInteger(value, "limit", 12, 1, 20) };
|
|
1503
|
+
},
|
|
1504
|
+
handler: async (input, context) => summarizeSymbolOverview(await getCachedSymbols(context), input)
|
|
1505
|
+
},
|
|
1506
|
+
{
|
|
1507
|
+
name: "market_list_symbols",
|
|
1508
|
+
title: "List Spot Symbols",
|
|
1509
|
+
description: "List one bounded page of configured spot symbols, optionally restricted to a quote asset. This response excludes precision and trading-rule metadata. Use market_get_symbol_info for one exact symbol's rules.",
|
|
1510
|
+
cliPath: ["market", "symbols-list"],
|
|
1511
|
+
module: "spot-common",
|
|
1512
|
+
access: "public",
|
|
1513
|
+
operation: "read",
|
|
1514
|
+
riskLevel: "low",
|
|
1515
|
+
inputSchema: { type: "object", properties: { quoteAsset: { type: "string" }, offset: { type: "integer", minimum: 0, description: "Zero-based page offset. Defaults to 0." }, limit: { type: "integer", minimum: 1, maximum: 50, description: "Page size. Defaults to 20." } }, additionalProperties: false },
|
|
1516
|
+
errorCodes: readErrors,
|
|
1517
|
+
validate: (input) => {
|
|
1518
|
+
const value = strictObject(input, ["quoteAsset", "offset", "limit"]);
|
|
1519
|
+
return {
|
|
1520
|
+
quoteAsset: optionalString(value, "quoteAsset"),
|
|
1521
|
+
offset: optionalInteger(value, "offset", 0, 0, Number.MAX_SAFE_INTEGER),
|
|
1522
|
+
limit: optionalInteger(value, "limit", 20, 1, 50)
|
|
1523
|
+
};
|
|
1524
|
+
},
|
|
1525
|
+
handler: async (input, context) => listSymbols(await getCachedSymbols(context), input)
|
|
1526
|
+
},
|
|
1376
1527
|
{
|
|
1377
1528
|
name: "market_search_symbols",
|
|
1378
1529
|
title: "Search Spot Symbols",
|
|
1379
|
-
description: "Search
|
|
1530
|
+
description: "Search configured spot symbols by a required keyword and return a small matching set without trading-rule metadata. Use market_list_symbols for browsing, or market_get_symbol_info for exact precision and minimum rules.",
|
|
1380
1531
|
cliPath: ["market", "symbols-search"],
|
|
1381
1532
|
module: "spot-common",
|
|
1382
1533
|
access: "public",
|
|
1383
1534
|
operation: "read",
|
|
1384
1535
|
riskLevel: "low",
|
|
1385
|
-
inputSchema: { type: "object", properties: { query: { type: "string" }, quoteAsset: { type: "string" }, limit: { type: "integer", minimum: 1, maximum:
|
|
1536
|
+
inputSchema: { type: "object", properties: { query: { type: "string", minLength: 1, description: "Required asset or symbol keyword, for example BTC." }, quoteAsset: { type: "string" }, limit: { type: "integer", minimum: 1, maximum: 20 } }, required: ["query"], additionalProperties: false },
|
|
1386
1537
|
errorCodes: readErrors,
|
|
1387
1538
|
validate: (input) => {
|
|
1388
1539
|
const value = strictObject(input, ["query", "quoteAsset", "limit"]);
|
|
1389
|
-
return { query:
|
|
1540
|
+
return { query: requiredString(value, "query"), quoteAsset: optionalString(value, "quoteAsset"), limit: optionalInteger(value, "limit", 10, 1, 20) };
|
|
1390
1541
|
},
|
|
1391
1542
|
handler: async (input, context) => {
|
|
1392
1543
|
const value = input;
|
|
1393
|
-
return
|
|
1544
|
+
return searchSymbols(await getCachedSymbols(context), value);
|
|
1394
1545
|
}
|
|
1395
1546
|
},
|
|
1547
|
+
{
|
|
1548
|
+
name: "market_get_symbol_info",
|
|
1549
|
+
title: "Get Spot Symbol Information",
|
|
1550
|
+
description: "Get the exact configured spot symbol's trading-rule metadata, including price and quantity precision plus minimum order constraints. Requires an exact symbol such as BTCUSDT or BTC/USDT.",
|
|
1551
|
+
cliPath: ["market", "symbol-info"],
|
|
1552
|
+
module: "spot-common",
|
|
1553
|
+
access: "public",
|
|
1554
|
+
operation: "read",
|
|
1555
|
+
riskLevel: "low",
|
|
1556
|
+
inputSchema: { type: "object", properties: { symbol: { type: "string", minLength: 1 } }, required: ["symbol"], additionalProperties: false },
|
|
1557
|
+
errorCodes: symbolReadErrors,
|
|
1558
|
+
validate: (input) => {
|
|
1559
|
+
const value = strictObject(input, ["symbol"]);
|
|
1560
|
+
return { symbol: requiredString(value, "symbol") };
|
|
1561
|
+
},
|
|
1562
|
+
handler: async (input, context) => getSymbolInfo(await getCachedSymbols(context), input.symbol)
|
|
1563
|
+
},
|
|
1396
1564
|
{
|
|
1397
1565
|
name: "market_get_ticker",
|
|
1398
1566
|
title: "Get Spot Ticker",
|
|
@@ -2643,10 +2811,10 @@ function createServer(profileName, readOnly) {
|
|
|
2643
2811
|
const registry = createToolRegistry();
|
|
2644
2812
|
const writeExecutor = new ToolWriteExecutor(registry);
|
|
2645
2813
|
const server = new Server(
|
|
2646
|
-
{ name: "ai-hub-agent-trade", version: "0.1.
|
|
2814
|
+
{ name: "ai-hub-agent-trade", version: "0.1.9" },
|
|
2647
2815
|
{
|
|
2648
2816
|
capabilities: { tools: {} },
|
|
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
|
|
2817
|
+
instructions: "Every successful read-tool response provides both JSON text and MCP structuredContent in the envelope { ok: true, data: ... }. All read-tool data is normalized: dataType=array means use data.items and data.count; dataType=object or scalar means use data.value; dataType=null means no value. Inspect data.dataType before formatting and never assume undocumented nested keys. For a generic trading-pair list, call market_get_symbol_overview first; it returns only counts and a small sample. For paged or quote-asset browsing, use market_list_symbols. Use market_search_symbols only when the user gave a keyword; query is required. Use market_get_symbol_info only for exact precision and minimum rules. Do not use a symbol search as a generic all-symbol list. The complete raw symbols payload is intentionally unavailable in MCP. For broad market questions, call market_get_ticker_summary, market_get_depth_summary, market_get_trades_summary, or market_get_klines_summary instead of fetching large raw payloads. Account types are fixed: 1=Spot, 2=Isolated Margin, 3=Cross Margin, 4=C2C, 5=Derivatives. Never call account type 2 Derivatives. For Spot to Derivatives, use account_prepare_transfer with fromAccount=EXCHANGE and toAccount=FUTURE. Use wallet_prepare_universal_transfer for margin or C2C; account type 2 requires its isolated-margin trading pair in symbol. Prepare/confirm tools return their documented action payload directly in data. For every state-changing action, call only a spot_prepare_* or margin_prepare_* tool first and show its exact summary to the user. Stop and wait for a new, explicit user confirmation message. Only then call confirm_action with that new message verbatim in userConfirmation. Never call prepare and confirm consecutively for one user instruction; never infer confirmation from prior intent, silence, or an Agent-generated message. For spot and margin orders, MARKET BUY always uses quoteAmount (the quote asset to spend); MARKET SELL uses baseQuantity (the base asset to sell). Never reinterpret a requested base quantity as quoteAmount."
|
|
2650
2818
|
}
|
|
2651
2819
|
);
|
|
2652
2820
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
@@ -2694,7 +2862,7 @@ function createServer(profileName, readOnly) {
|
|
|
2694
2862
|
}
|
|
2695
2863
|
const tool = registry.byName(request.params.name, { readOnly });
|
|
2696
2864
|
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
|
|
2865
|
+
throw new AiHubError("AI_HUB_TOOL_NOT_AVAILABLE", `Tool "${request.params.name}" is not available through MCP. Use market_get_symbol_overview, market_list_symbols, market_search_symbols, or market_get_symbol_info instead.`);
|
|
2698
2866
|
}
|
|
2699
2867
|
const data = await registry.execute(request.params.name, request.params.arguments ?? {}, context, { readOnly });
|
|
2700
2868
|
return toMcpReadResult(formatMcpData(data));
|