@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.
Files changed (3) hide show
  1. package/README.md +5 -2
  2. package/dist/index.js +262 -78
  3. 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 for five minutes. Known quantity/price precision and limit-order minimum violations are rejected before confirmation.
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
- - `market_search_symbols`: bounded symbol search and metadata. Use this for every MCP symbol lookup; it returns matching rows in `data.value.items`.
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 expiresAtMs = this.now() + this.ttlMs;
79
- const confirmationId = randomUUID();
80
- const requestHash = hash(input);
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 { action: pending.action, payload: pending.payload, requestHash: pending.requestHash };
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 = homedir()) {
406
- return join(home, CONFIG_DIRECTORY, CONFIG_FILENAME);
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 readFile(this.filePath, "utf8"));
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 mkdir(directory, { recursive: true, mode: 448 });
542
- await chmod(directory, 448);
557
+ await mkdir2(directory, { recursive: true, mode: 448 });
558
+ await chmod2(directory, 448);
543
559
  const temporaryPath = `${this.filePath}.${process.pid}.tmp`;
544
- await writeFile(temporaryPath, stringify(config), { mode: 384 });
545
- await chmod(temporaryPath, 384);
546
- await rename(temporaryPath, this.filePath);
547
- await chmod(this.filePath, 384);
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 summarizeSymbols(value, options) {
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
- const query = options.query?.trim().toUpperCase();
1039
- const quoteAsset = options.quoteAsset?.trim().toUpperCase();
1040
- const matches = rows2.filter((item) => {
1041
- const symbol = text(item.SymbolName) ?? text(item.symbol) ?? "";
1042
- const quote = text(item.quoteAsset)?.toUpperCase();
1043
- return (!query || symbol.toUpperCase().includes(query)) && (!quoteAsset || quote === quoteAsset);
1044
- });
1045
- const sortedMatches = [...matches].sort((left, right) => {
1046
- if (!query) return 0;
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 = (text(item.SymbolName) ?? text(item.symbol) ?? "").toUpperCase();
1049
- return symbol.startsWith(`${query}/`) ? 0 : symbol.startsWith(query) ? 1 : 2;
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
- const quoteAssetCounts = /* @__PURE__ */ new Map();
1054
- for (const item of rows2) {
1055
- const quote = text(item.quoteAsset)?.toUpperCase();
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
- quoteAssetCounts: [...quoteAssetCounts.entries()].sort((left, right) => right[1] - left[1]).map(([asset, count]) => ({ asset, count })),
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 normalizedSymbol(symbol) {
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(normalizedSymbol(symbol), {
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 = context.api.symbols().then((response) => {
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(normalizedSymbol(symbol));
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 browsing or filtering, because this response may be large.",
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 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.",
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: 50 } }, additionalProperties: false },
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: optionalString(value, "query"), quoteAsset: optionalString(value, "quoteAsset"), limit: optionalInteger(value, "limit", 20, 1, 50) };
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 summarizeSymbols(await getCachedSymbols(context), value);
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.8" },
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 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. 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."
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 for symbol lookup.`);
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));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aihubspot/agent-trade-mcp",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "description": "Local stdio MCP server for AI Hub spot trading tools",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {