@aihubspot/agent-trade-mcp 0.1.8 → 0.1.10
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 +262 -78
- 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
|
@@ -56,6 +56,9 @@ function loadStoredCredentials(apiKey, secretKey) {
|
|
|
56
56
|
|
|
57
57
|
// ../core/src/confirmation.ts
|
|
58
58
|
import { createHash as createHash2, randomUUID } from "crypto";
|
|
59
|
+
import { chmod, mkdir, readFile, rename, rm, writeFile } from "fs/promises";
|
|
60
|
+
import { homedir } from "os";
|
|
61
|
+
import { join } from "path";
|
|
59
62
|
function stableJson(value) {
|
|
60
63
|
if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
|
|
61
64
|
if (value && typeof value === "object") {
|
|
@@ -66,6 +69,37 @@ function stableJson(value) {
|
|
|
66
69
|
function hash(input) {
|
|
67
70
|
return createHash2("sha256").update(stableJson({ action: input.action, payload: input.payload, context: input.context })).digest("hex");
|
|
68
71
|
}
|
|
72
|
+
function createPending(input, ttlMs, now) {
|
|
73
|
+
return {
|
|
74
|
+
...input,
|
|
75
|
+
confirmationId: randomUUID(),
|
|
76
|
+
requestHash: hash(input),
|
|
77
|
+
expiresAtMs: now() + ttlMs
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function preparedAction(pending) {
|
|
81
|
+
return {
|
|
82
|
+
confirmationId: pending.confirmationId,
|
|
83
|
+
expiresAt: new Date(pending.expiresAtMs).toISOString(),
|
|
84
|
+
requestHash: pending.requestHash,
|
|
85
|
+
action: pending.action,
|
|
86
|
+
summary: pending.summary,
|
|
87
|
+
requiresNewUserConfirmation: true,
|
|
88
|
+
nextStep: "Stop and wait for a new explicit user confirmation message. Do not confirm from the same user instruction that prepared this request."
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function confirmPending(pending, userConfirmation, context, now) {
|
|
92
|
+
if (typeof userConfirmation !== "string" || !userConfirmation.trim()) {
|
|
93
|
+
throw new AiHubError("AI_HUB_CONFIRMATION_REQUIRED", "A non-empty new explicit user confirmation message is required before a state-changing request can execute.");
|
|
94
|
+
}
|
|
95
|
+
if (pending.expiresAtMs < now()) {
|
|
96
|
+
throw new AiHubError("AI_HUB_CONFIRMATION_EXPIRED", "Confirmation has expired.");
|
|
97
|
+
}
|
|
98
|
+
if (stableJson(pending.context) !== stableJson(context)) {
|
|
99
|
+
throw new AiHubError("AI_HUB_CONFIRMATION_CONTEXT_CHANGED", "Profile, OpenAPI base URL, configuration, or credentials changed after prepare.");
|
|
100
|
+
}
|
|
101
|
+
return { action: pending.action, payload: pending.payload, requestHash: pending.requestHash };
|
|
102
|
+
}
|
|
69
103
|
var ConfirmationService = class {
|
|
70
104
|
constructor(ttlMs = 5 * 60 * 1e3, now = () => Date.now()) {
|
|
71
105
|
this.ttlMs = ttlMs;
|
|
@@ -75,19 +109,9 @@ var ConfirmationService = class {
|
|
|
75
109
|
now;
|
|
76
110
|
actions = /* @__PURE__ */ new Map();
|
|
77
111
|
prepare(input) {
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
this.actions.set(confirmationId, { ...input, confirmationId, requestHash, expiresAtMs, consumed: false });
|
|
82
|
-
return {
|
|
83
|
-
confirmationId,
|
|
84
|
-
expiresAt: new Date(expiresAtMs).toISOString(),
|
|
85
|
-
requestHash,
|
|
86
|
-
action: input.action,
|
|
87
|
-
summary: input.summary,
|
|
88
|
-
requiresNewUserConfirmation: true,
|
|
89
|
-
nextStep: "Stop and wait for a new explicit user confirmation message. Do not call confirm_action from the same user instruction that prepared this request."
|
|
90
|
-
};
|
|
112
|
+
const pending = createPending(input, this.ttlMs, this.now);
|
|
113
|
+
this.actions.set(pending.confirmationId, { ...pending, consumed: false });
|
|
114
|
+
return preparedAction(pending);
|
|
91
115
|
}
|
|
92
116
|
confirm(confirmationId, userConfirmation, context) {
|
|
93
117
|
if (typeof userConfirmation !== "string" || !userConfirmation.trim()) {
|
|
@@ -96,17 +120,9 @@ var ConfirmationService = class {
|
|
|
96
120
|
const pending = this.actions.get(confirmationId);
|
|
97
121
|
if (!pending) throw new AiHubError("AI_HUB_CONFIRMATION_NOT_FOUND", "Confirmation was not found or has already been consumed.");
|
|
98
122
|
if (pending.consumed) throw new AiHubError("AI_HUB_CONFIRMATION_CONSUMED", "Confirmation has already been consumed.");
|
|
99
|
-
if (pending.expiresAtMs < this.now()) {
|
|
100
|
-
this.actions.delete(confirmationId);
|
|
101
|
-
throw new AiHubError("AI_HUB_CONFIRMATION_EXPIRED", "Confirmation has expired.");
|
|
102
|
-
}
|
|
103
|
-
if (stableJson(pending.context) !== stableJson(context)) {
|
|
104
|
-
this.actions.delete(confirmationId);
|
|
105
|
-
throw new AiHubError("AI_HUB_CONFIRMATION_CONTEXT_CHANGED", "Profile, OpenAPI base URL, configuration, or credentials changed after prepare.");
|
|
106
|
-
}
|
|
107
123
|
pending.consumed = true;
|
|
108
124
|
this.actions.delete(confirmationId);
|
|
109
|
-
return
|
|
125
|
+
return confirmPending(pending, userConfirmation, context, this.now);
|
|
110
126
|
}
|
|
111
127
|
};
|
|
112
128
|
|
|
@@ -394,16 +410,16 @@ var AiHubSpotApi = class {
|
|
|
394
410
|
// ../core/src/config.ts
|
|
395
411
|
import { createHash as createHash3 } from "crypto";
|
|
396
412
|
import { lookup } from "dns/promises";
|
|
397
|
-
import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
398
|
-
import { homedir } from "os";
|
|
399
|
-
import { dirname, join } from "path";
|
|
413
|
+
import { chmod as chmod2, mkdir as mkdir2, readFile as readFile2, rename as rename2, writeFile as writeFile2 } from "fs/promises";
|
|
414
|
+
import { homedir as homedir2 } from "os";
|
|
415
|
+
import { dirname, join as join2 } from "path";
|
|
400
416
|
import { isIP } from "net";
|
|
401
417
|
import { parse, stringify } from "smol-toml";
|
|
402
418
|
var CONFIG_DIRECTORY = ".ai-hub";
|
|
403
419
|
var CONFIG_FILENAME = "config.toml";
|
|
404
420
|
var PROFILE_NAME = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/;
|
|
405
|
-
function configFilePath(home =
|
|
406
|
-
return
|
|
421
|
+
function configFilePath(home = homedir2()) {
|
|
422
|
+
return join2(home, CONFIG_DIRECTORY, CONFIG_FILENAME);
|
|
407
423
|
}
|
|
408
424
|
function validateProfileName(name) {
|
|
409
425
|
if (!PROFILE_NAME.test(name)) {
|
|
@@ -467,7 +483,7 @@ var ConfigStore = class {
|
|
|
467
483
|
filePath;
|
|
468
484
|
async read() {
|
|
469
485
|
try {
|
|
470
|
-
return parseConfig(await
|
|
486
|
+
return parseConfig(await readFile2(this.filePath, "utf8"));
|
|
471
487
|
} catch (error) {
|
|
472
488
|
if (error.code === "ENOENT") return defaultConfig();
|
|
473
489
|
throw error;
|
|
@@ -538,13 +554,13 @@ var ConfigStore = class {
|
|
|
538
554
|
}
|
|
539
555
|
async write(config) {
|
|
540
556
|
const directory = dirname(this.filePath);
|
|
541
|
-
await
|
|
542
|
-
await
|
|
557
|
+
await mkdir2(directory, { recursive: true, mode: 448 });
|
|
558
|
+
await chmod2(directory, 448);
|
|
543
559
|
const temporaryPath = `${this.filePath}.${process.pid}.tmp`;
|
|
544
|
-
await
|
|
545
|
-
await
|
|
546
|
-
await
|
|
547
|
-
await
|
|
560
|
+
await writeFile2(temporaryPath, stringify(config), { mode: 384 });
|
|
561
|
+
await chmod2(temporaryPath, 384);
|
|
562
|
+
await rename2(temporaryPath, this.filePath);
|
|
563
|
+
await chmod2(this.filePath, 384);
|
|
548
564
|
}
|
|
549
565
|
};
|
|
550
566
|
|
|
@@ -1032,47 +1048,112 @@ function summarizeTickers(value, options) {
|
|
|
1032
1048
|
topByQuoteVolume: [...rows2].sort(compareByNumericField("amount", "desc")).slice(0, limit).map(tickerItem)
|
|
1033
1049
|
};
|
|
1034
1050
|
}
|
|
1035
|
-
function
|
|
1051
|
+
function normalizedSymbol(value) {
|
|
1052
|
+
return value.replaceAll("/", "").trim().toUpperCase();
|
|
1053
|
+
}
|
|
1054
|
+
function parsedSymbolRows(value) {
|
|
1036
1055
|
const payload = record(value, "Symbols response must be an object.");
|
|
1037
1056
|
const rows2 = records(payload.symbols, "Symbols response must contain a symbols array.");
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1057
|
+
return rows2.map((item) => {
|
|
1058
|
+
const apiSymbol = text(item.symbol);
|
|
1059
|
+
const symbol = text(item.SymbolName) ?? apiSymbol ?? "";
|
|
1060
|
+
return {
|
|
1061
|
+
symbol,
|
|
1062
|
+
apiSymbol,
|
|
1063
|
+
baseAsset: text(item.baseAssetName) ?? text(item.baseAsset),
|
|
1064
|
+
quoteAsset: text(item.quoteAssetName) ?? text(item.quoteAsset),
|
|
1065
|
+
pricePrecision: item.pricePrecision ?? null,
|
|
1066
|
+
quantityPrecision: item.quantityPrecision ?? null,
|
|
1067
|
+
limitVolumeMin: text(item.limitVolumeMin),
|
|
1068
|
+
limitPriceMin: text(item.limitPriceMin),
|
|
1069
|
+
limitAmountMin: text(item.limitAmountMin)
|
|
1070
|
+
};
|
|
1071
|
+
}).filter((item) => Boolean(item.symbol));
|
|
1072
|
+
}
|
|
1073
|
+
function quoteAssetCounts(rows2) {
|
|
1074
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1075
|
+
for (const item of rows2) {
|
|
1076
|
+
const quote = item.quoteAsset?.toUpperCase();
|
|
1077
|
+
if (quote) counts.set(quote, (counts.get(quote) ?? 0) + 1);
|
|
1078
|
+
}
|
|
1079
|
+
return [...counts.entries()].sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0])).map(([asset, count]) => ({ asset, count }));
|
|
1080
|
+
}
|
|
1081
|
+
function basicSymbolItem(item) {
|
|
1082
|
+
return {
|
|
1083
|
+
symbol: item.symbol,
|
|
1084
|
+
apiSymbol: item.apiSymbol,
|
|
1085
|
+
baseAsset: item.baseAsset,
|
|
1086
|
+
quoteAsset: item.quoteAsset
|
|
1087
|
+
};
|
|
1088
|
+
}
|
|
1089
|
+
function fullSymbolItem(item) {
|
|
1090
|
+
return {
|
|
1091
|
+
...basicSymbolItem(item),
|
|
1092
|
+
pricePrecision: item.pricePrecision,
|
|
1093
|
+
quantityPrecision: item.quantityPrecision,
|
|
1094
|
+
limitVolumeMin: item.limitVolumeMin,
|
|
1095
|
+
limitPriceMin: item.limitPriceMin,
|
|
1096
|
+
limitAmountMin: item.limitAmountMin
|
|
1097
|
+
};
|
|
1098
|
+
}
|
|
1099
|
+
function sortSymbolMatches(rows2, query) {
|
|
1100
|
+
const normalizedQuery = query?.trim().toUpperCase();
|
|
1101
|
+
return [...rows2].sort((left, right) => {
|
|
1102
|
+
if (!normalizedQuery) return left.symbol.localeCompare(right.symbol);
|
|
1047
1103
|
const rank = (item) => {
|
|
1048
|
-
const symbol =
|
|
1049
|
-
return symbol.startsWith(`${
|
|
1104
|
+
const symbol = item.symbol.toUpperCase();
|
|
1105
|
+
return symbol.startsWith(`${normalizedQuery}/`) ? 0 : symbol.startsWith(normalizedQuery) ? 1 : 2;
|
|
1050
1106
|
};
|
|
1051
|
-
return rank(left) - rank(right);
|
|
1107
|
+
return rank(left) - rank(right) || left.symbol.localeCompare(right.symbol);
|
|
1052
1108
|
});
|
|
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
|
-
}));
|
|
1109
|
+
}
|
|
1110
|
+
function summarizeSymbolOverview(value, options) {
|
|
1111
|
+
const rows2 = parsedSymbolRows(value);
|
|
1069
1112
|
return {
|
|
1070
1113
|
totalSymbols: rows2.length,
|
|
1114
|
+
quoteAssetCounts: quoteAssetCounts(rows2),
|
|
1115
|
+
sampleSymbols: sortSymbolMatches(rows2).slice(0, options.limit).map((item) => item.symbol)
|
|
1116
|
+
};
|
|
1117
|
+
}
|
|
1118
|
+
function listSymbols(value, options) {
|
|
1119
|
+
const allRows = parsedSymbolRows(value);
|
|
1120
|
+
const quoteAsset = options.quoteAsset?.trim().toUpperCase();
|
|
1121
|
+
const matches = sortSymbolMatches(allRows.filter((item) => !quoteAsset || item.quoteAsset?.toUpperCase() === quoteAsset));
|
|
1122
|
+
const items = matches.slice(options.offset, options.offset + options.limit).map(basicSymbolItem);
|
|
1123
|
+
const nextOffset = options.offset + items.length;
|
|
1124
|
+
return {
|
|
1125
|
+
totalSymbols: allRows.length,
|
|
1071
1126
|
matchedSymbols: matches.length,
|
|
1072
|
-
|
|
1127
|
+
quoteAsset: quoteAsset ?? null,
|
|
1128
|
+
offset: options.offset,
|
|
1129
|
+
limit: options.limit,
|
|
1130
|
+
nextOffset: nextOffset < matches.length ? nextOffset : null,
|
|
1073
1131
|
items
|
|
1074
1132
|
};
|
|
1075
1133
|
}
|
|
1134
|
+
function searchSymbols(value, options) {
|
|
1135
|
+
const allRows = parsedSymbolRows(value);
|
|
1136
|
+
const query = options.query.trim().toUpperCase();
|
|
1137
|
+
const quoteAsset = options.quoteAsset?.trim().toUpperCase();
|
|
1138
|
+
const matches = allRows.filter((item) => {
|
|
1139
|
+
const symbol = item.symbol.toUpperCase();
|
|
1140
|
+
const apiSymbol = item.apiSymbol?.toUpperCase() ?? "";
|
|
1141
|
+
return (symbol.includes(query) || apiSymbol.includes(query)) && (!quoteAsset || item.quoteAsset?.toUpperCase() === quoteAsset);
|
|
1142
|
+
});
|
|
1143
|
+
const sortedMatches = sortSymbolMatches(matches, query);
|
|
1144
|
+
return {
|
|
1145
|
+
matchedSymbols: matches.length,
|
|
1146
|
+
query,
|
|
1147
|
+
quoteAsset: quoteAsset ?? null,
|
|
1148
|
+
items: sortedMatches.slice(0, options.limit).map(basicSymbolItem)
|
|
1149
|
+
};
|
|
1150
|
+
}
|
|
1151
|
+
function getSymbolInfo(value, symbol) {
|
|
1152
|
+
const target = normalizedSymbol(symbol);
|
|
1153
|
+
const item = parsedSymbolRows(value).find((row) => normalizedSymbol(row.symbol) === target || row.apiSymbol !== null && normalizedSymbol(row.apiSymbol) === target);
|
|
1154
|
+
if (!item) throw new AiHubError("AI_HUB_SYMBOL_NOT_FOUND", `Symbol "${symbol}" was not returned by the configured tenant OpenAPI symbols endpoint.`);
|
|
1155
|
+
return fullSymbolItem(item);
|
|
1156
|
+
}
|
|
1076
1157
|
function level(value) {
|
|
1077
1158
|
if (!Array.isArray(value)) return { price: null, quantity: null };
|
|
1078
1159
|
return { price: text(value[0]), quantity: text(value[1]) };
|
|
@@ -1169,13 +1250,53 @@ function summarizeKlines(value, symbol, interval) {
|
|
|
1169
1250
|
}
|
|
1170
1251
|
|
|
1171
1252
|
// ../core/src/tools/symbol-rules.ts
|
|
1253
|
+
import { createHash as createHash4 } from "crypto";
|
|
1254
|
+
import { chmod as chmod3, mkdir as mkdir3, readFile as readFile3, rename as rename3, rm as rm2, writeFile as writeFile3 } from "fs/promises";
|
|
1255
|
+
import { homedir as homedir3 } from "os";
|
|
1256
|
+
import { join as join3 } from "path";
|
|
1172
1257
|
var CACHE_TTL_MS = 60 * 60 * 1e3;
|
|
1173
1258
|
var cache = /* @__PURE__ */ new Map();
|
|
1174
1259
|
var pendingLoads = /* @__PURE__ */ new Map();
|
|
1175
1260
|
function cacheKey(context) {
|
|
1176
1261
|
return `${context.profile.name}\0${context.profile.configVersion}\0${context.profile.openApiBaseUrl}`;
|
|
1177
1262
|
}
|
|
1178
|
-
function
|
|
1263
|
+
function persistentCacheEnabled() {
|
|
1264
|
+
return process.env.AI_HUB_DISABLE_PERSISTENT_CACHE !== "1";
|
|
1265
|
+
}
|
|
1266
|
+
function persistentCacheDirectory() {
|
|
1267
|
+
return process.env.AI_HUB_CACHE_DIR ?? join3(homedir3(), ".ai-hub", "cache");
|
|
1268
|
+
}
|
|
1269
|
+
function persistentCachePath(key) {
|
|
1270
|
+
const hash2 = createHash4("sha256").update(key).digest("hex");
|
|
1271
|
+
return join3(persistentCacheDirectory(), `symbols-${hash2}.json`);
|
|
1272
|
+
}
|
|
1273
|
+
async function loadPersistentSnapshot(key) {
|
|
1274
|
+
if (!persistentCacheEnabled()) return void 0;
|
|
1275
|
+
try {
|
|
1276
|
+
const parsed = JSON.parse(await readFile3(persistentCachePath(key), "utf8"));
|
|
1277
|
+
if (typeof parsed.expiresAt !== "number" || !Number.isFinite(parsed.expiresAt) || parsed.expiresAt <= Date.now() || !("response" in parsed)) return void 0;
|
|
1278
|
+
return { expiresAt: parsed.expiresAt, response: parsed.response, rules: parseRules(parsed.response) };
|
|
1279
|
+
} catch {
|
|
1280
|
+
return void 0;
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
async function persistSnapshot(key, entry) {
|
|
1284
|
+
if (!persistentCacheEnabled()) return;
|
|
1285
|
+
const directory = persistentCacheDirectory();
|
|
1286
|
+
const filePath = persistentCachePath(key);
|
|
1287
|
+
const temporaryPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
1288
|
+
try {
|
|
1289
|
+
await mkdir3(directory, { recursive: true, mode: 448 });
|
|
1290
|
+
await chmod3(directory, 448);
|
|
1291
|
+
await writeFile3(temporaryPath, JSON.stringify({ expiresAt: entry.expiresAt, response: entry.response }), { mode: 384 });
|
|
1292
|
+
await chmod3(temporaryPath, 384);
|
|
1293
|
+
await rename3(temporaryPath, filePath);
|
|
1294
|
+
await chmod3(filePath, 384);
|
|
1295
|
+
} catch {
|
|
1296
|
+
await rm2(temporaryPath, { force: true }).catch(() => void 0);
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
function normalizedSymbol2(symbol) {
|
|
1179
1300
|
return symbol.replaceAll("/", "").trim().toUpperCase();
|
|
1180
1301
|
}
|
|
1181
1302
|
function stringValue(value) {
|
|
@@ -1202,7 +1323,7 @@ function parseRules(response) {
|
|
|
1202
1323
|
for (const row of rows(response)) {
|
|
1203
1324
|
const symbol = stringValue(row.symbol);
|
|
1204
1325
|
if (!symbol) continue;
|
|
1205
|
-
result2.set(
|
|
1326
|
+
result2.set(normalizedSymbol2(symbol), {
|
|
1206
1327
|
symbol,
|
|
1207
1328
|
quantityPrecision: nonNegativeInteger(row.quantityPrecision),
|
|
1208
1329
|
pricePrecision: nonNegativeInteger(row.pricePrecision),
|
|
@@ -1219,11 +1340,18 @@ async function loadSnapshot(context) {
|
|
|
1219
1340
|
if (current && current.expiresAt > Date.now()) return current;
|
|
1220
1341
|
const pending = pendingLoads.get(key);
|
|
1221
1342
|
if (pending) return pending;
|
|
1222
|
-
const load =
|
|
1343
|
+
const load = (async () => {
|
|
1344
|
+
const persisted = await loadPersistentSnapshot(key);
|
|
1345
|
+
if (persisted) {
|
|
1346
|
+
cache.set(key, persisted);
|
|
1347
|
+
return persisted;
|
|
1348
|
+
}
|
|
1349
|
+
const response = await context.api.symbols();
|
|
1223
1350
|
const entry = { response, rules: parseRules(response), expiresAt: Date.now() + CACHE_TTL_MS };
|
|
1224
1351
|
cache.set(key, entry);
|
|
1352
|
+
await persistSnapshot(key, entry);
|
|
1225
1353
|
return entry;
|
|
1226
|
-
}).finally(() => pendingLoads.delete(key));
|
|
1354
|
+
})().finally(() => pendingLoads.delete(key));
|
|
1227
1355
|
pendingLoads.set(key, load);
|
|
1228
1356
|
return load;
|
|
1229
1357
|
}
|
|
@@ -1232,7 +1360,7 @@ async function getCachedSymbols(context) {
|
|
|
1232
1360
|
}
|
|
1233
1361
|
async function getSymbolRule(context, symbol) {
|
|
1234
1362
|
const entry = await loadSnapshot(context);
|
|
1235
|
-
const rule = entry.rules.get(
|
|
1363
|
+
const rule = entry.rules.get(normalizedSymbol2(symbol));
|
|
1236
1364
|
if (!rule) throw new AiHubError("AI_HUB_SYMBOL_NOT_FOUND", `Symbol "${symbol}" was not returned by the configured tenant OpenAPI symbols endpoint.`);
|
|
1237
1365
|
return rule;
|
|
1238
1366
|
}
|
|
@@ -1289,6 +1417,7 @@ async function preflightSymbolOrder(context, order) {
|
|
|
1289
1417
|
|
|
1290
1418
|
// ../core/src/tools/market-tools.ts
|
|
1291
1419
|
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"];
|
|
1420
|
+
var symbolReadErrors = [...readErrors, "AI_HUB_SYMBOL_NOT_FOUND"];
|
|
1292
1421
|
var KLINE_INTERVALS = ["1min", "5min", "15min", "30min", "60min", "1day", "1week", "1month"];
|
|
1293
1422
|
var KLINE_INTERVAL_ALIASES = {
|
|
1294
1423
|
"1m": "1min",
|
|
@@ -1361,7 +1490,7 @@ var marketTools = [
|
|
|
1361
1490
|
{
|
|
1362
1491
|
name: "market_get_symbols",
|
|
1363
1492
|
title: "Get Spot Symbols",
|
|
1364
|
-
description: "Get complete spot symbol metadata from the configured tenant OpenAPI. Use market_search_symbols for
|
|
1493
|
+
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
1494
|
cliPath: ["market", "symbols"],
|
|
1366
1495
|
module: "spot-common",
|
|
1367
1496
|
access: "public",
|
|
@@ -1373,26 +1502,81 @@ var marketTools = [
|
|
|
1373
1502
|
validate: (input) => strictObject(input, []),
|
|
1374
1503
|
handler: (_input, context) => getCachedSymbols(context)
|
|
1375
1504
|
},
|
|
1505
|
+
{
|
|
1506
|
+
name: "market_get_symbol_overview",
|
|
1507
|
+
title: "Get Spot Symbol Overview",
|
|
1508
|
+
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.",
|
|
1509
|
+
cliPath: ["market", "symbols-overview"],
|
|
1510
|
+
module: "spot-common",
|
|
1511
|
+
access: "public",
|
|
1512
|
+
operation: "read",
|
|
1513
|
+
riskLevel: "low",
|
|
1514
|
+
inputSchema: { type: "object", properties: { limit: { type: "integer", minimum: 1, maximum: 20, description: "Number of sample display symbols. Defaults to 12." } }, additionalProperties: false },
|
|
1515
|
+
errorCodes: readErrors,
|
|
1516
|
+
validate: (input) => {
|
|
1517
|
+
const value = strictObject(input, ["limit"]);
|
|
1518
|
+
return { limit: optionalInteger(value, "limit", 12, 1, 20) };
|
|
1519
|
+
},
|
|
1520
|
+
handler: async (input, context) => summarizeSymbolOverview(await getCachedSymbols(context), input)
|
|
1521
|
+
},
|
|
1522
|
+
{
|
|
1523
|
+
name: "market_list_symbols",
|
|
1524
|
+
title: "List Spot Symbols",
|
|
1525
|
+
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.",
|
|
1526
|
+
cliPath: ["market", "symbols-list"],
|
|
1527
|
+
module: "spot-common",
|
|
1528
|
+
access: "public",
|
|
1529
|
+
operation: "read",
|
|
1530
|
+
riskLevel: "low",
|
|
1531
|
+
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 },
|
|
1532
|
+
errorCodes: readErrors,
|
|
1533
|
+
validate: (input) => {
|
|
1534
|
+
const value = strictObject(input, ["quoteAsset", "offset", "limit"]);
|
|
1535
|
+
return {
|
|
1536
|
+
quoteAsset: optionalString(value, "quoteAsset"),
|
|
1537
|
+
offset: optionalInteger(value, "offset", 0, 0, Number.MAX_SAFE_INTEGER),
|
|
1538
|
+
limit: optionalInteger(value, "limit", 20, 1, 50)
|
|
1539
|
+
};
|
|
1540
|
+
},
|
|
1541
|
+
handler: async (input, context) => listSymbols(await getCachedSymbols(context), input)
|
|
1542
|
+
},
|
|
1376
1543
|
{
|
|
1377
1544
|
name: "market_search_symbols",
|
|
1378
1545
|
title: "Search Spot Symbols",
|
|
1379
|
-
description: "Search
|
|
1546
|
+
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
1547
|
cliPath: ["market", "symbols-search"],
|
|
1381
1548
|
module: "spot-common",
|
|
1382
1549
|
access: "public",
|
|
1383
1550
|
operation: "read",
|
|
1384
1551
|
riskLevel: "low",
|
|
1385
|
-
inputSchema: { type: "object", properties: { query: { type: "string" }, quoteAsset: { type: "string" }, limit: { type: "integer", minimum: 1, maximum:
|
|
1552
|
+
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
1553
|
errorCodes: readErrors,
|
|
1387
1554
|
validate: (input) => {
|
|
1388
1555
|
const value = strictObject(input, ["query", "quoteAsset", "limit"]);
|
|
1389
|
-
return { query:
|
|
1556
|
+
return { query: requiredString(value, "query"), quoteAsset: optionalString(value, "quoteAsset"), limit: optionalInteger(value, "limit", 10, 1, 20) };
|
|
1390
1557
|
},
|
|
1391
1558
|
handler: async (input, context) => {
|
|
1392
1559
|
const value = input;
|
|
1393
|
-
return
|
|
1560
|
+
return searchSymbols(await getCachedSymbols(context), value);
|
|
1394
1561
|
}
|
|
1395
1562
|
},
|
|
1563
|
+
{
|
|
1564
|
+
name: "market_get_symbol_info",
|
|
1565
|
+
title: "Get Spot Symbol Information",
|
|
1566
|
+
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.",
|
|
1567
|
+
cliPath: ["market", "symbol-info"],
|
|
1568
|
+
module: "spot-common",
|
|
1569
|
+
access: "public",
|
|
1570
|
+
operation: "read",
|
|
1571
|
+
riskLevel: "low",
|
|
1572
|
+
inputSchema: { type: "object", properties: { symbol: { type: "string", minLength: 1 } }, required: ["symbol"], additionalProperties: false },
|
|
1573
|
+
errorCodes: symbolReadErrors,
|
|
1574
|
+
validate: (input) => {
|
|
1575
|
+
const value = strictObject(input, ["symbol"]);
|
|
1576
|
+
return { symbol: requiredString(value, "symbol") };
|
|
1577
|
+
},
|
|
1578
|
+
handler: async (input, context) => getSymbolInfo(await getCachedSymbols(context), input.symbol)
|
|
1579
|
+
},
|
|
1396
1580
|
{
|
|
1397
1581
|
name: "market_get_ticker",
|
|
1398
1582
|
title: "Get Spot Ticker",
|
|
@@ -2343,7 +2527,7 @@ var ToolRegistry = class {
|
|
|
2343
2527
|
if (tool.operation !== "write" || !tool.writeSummary) throw new AiHubError("AI_HUB_TOOL_NOT_WRITE", `Tool "${name}" is not a confirmable write Tool.`);
|
|
2344
2528
|
const validInput = tool.validate(input);
|
|
2345
2529
|
const preflightInput = tool.preflight ? await tool.preflight(validInput, context) : validInput;
|
|
2346
|
-
return confirmations.prepare({
|
|
2530
|
+
return await confirmations.prepare({
|
|
2347
2531
|
action: tool.name,
|
|
2348
2532
|
payload: preflightInput,
|
|
2349
2533
|
context: confirmationContext(context),
|
|
@@ -2643,10 +2827,10 @@ function createServer(profileName, readOnly) {
|
|
|
2643
2827
|
const registry = createToolRegistry();
|
|
2644
2828
|
const writeExecutor = new ToolWriteExecutor(registry);
|
|
2645
2829
|
const server = new Server(
|
|
2646
|
-
{ name: "ai-hub-agent-trade", version: "0.1.
|
|
2830
|
+
{ name: "ai-hub-agent-trade", version: "0.1.10" },
|
|
2647
2831
|
{
|
|
2648
2832
|
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
|
|
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."
|
|
2650
2834
|
}
|
|
2651
2835
|
);
|
|
2652
2836
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
@@ -2694,7 +2878,7 @@ function createServer(profileName, readOnly) {
|
|
|
2694
2878
|
}
|
|
2695
2879
|
const tool = registry.byName(request.params.name, { readOnly });
|
|
2696
2880
|
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
|
|
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.`);
|
|
2698
2882
|
}
|
|
2699
2883
|
const data = await registry.execute(request.params.name, request.params.arguments ?? {}, context, { readOnly });
|
|
2700
2884
|
return toMcpReadResult(formatMcpData(data));
|