@aihubspot/agent-trade-mcp 0.1.4 → 0.1.6

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 +14 -2
  2. package/dist/index.js +387 -21
  3. package/package.json +6 -1
package/README.md CHANGED
@@ -25,7 +25,7 @@ Use `spot_prepare_market_buy` or `margin_prepare_market_buy` only with `quoteAmo
25
25
 
26
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.
27
27
 
28
- ## Ticker response
28
+ ## Read response
29
29
 
30
30
  All read tools return `{ "ok": true, "data": ... }` in one of these stable forms:
31
31
 
@@ -33,4 +33,16 @@ All read tools return `{ "ok": true, "data": ... }` in one of these stable forms
33
33
  - Objects: `{ "dataType": "object", "value": { ... } }`
34
34
  - Scalars: `{ "dataType": "scalar", "value": "..." }`
35
35
 
36
- `market_get_ticker` additionally exposes `data.tickers` as an alias of `data.items`. Check `data.dataType` before formatting; do not infer raw OpenAPI nesting.
36
+ MCP clients that support the current protocol also receive the same envelope in `structuredContent`, validated by each read tool's output schema. Check `data.dataType` before formatting; do not infer raw OpenAPI nesting.
37
+
38
+ ## Bounded market analysis
39
+
40
+ Use these tools for requests that would otherwise return a broad market payload:
41
+
42
+ - `market_search_symbols`: bounded symbol search and metadata.
43
+ - `market_get_ticker_summary`: watchlist, gainers, losers, and quote-volume leaders.
44
+ - `market_get_depth_summary`: best bid/ask, spread, and up to 20 levels per side.
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.
47
+
48
+ The raw symbol, ticker, depth, trades, and kline tools remain available only when an exact full payload is explicitly needed. Raw trades and klines are capped at 100 rows by this client.
package/dist/index.js CHANGED
@@ -434,8 +434,8 @@ async function normalizeOpenApiBaseUrl(value) {
434
434
  if (url.hostname.toLowerCase() === "localhost") {
435
435
  throw new AiHubError("AI_HUB_UNSAFE_OPENAPI_URL", "Localhost is not allowed as an OpenAPI base URL.");
436
436
  }
437
- const records = isIP(url.hostname) ? [{ address: url.hostname }] : await lookup(url.hostname, { all: true, verbatim: true });
438
- if (records.length === 0 || records.some((record) => blockedIp(record.address))) {
437
+ const records2 = isIP(url.hostname) ? [{ address: url.hostname }] : await lookup(url.hostname, { all: true, verbatim: true });
438
+ if (records2.length === 0 || records2.some((record2) => blockedIp(record2.address))) {
439
439
  throw new AiHubError("AI_HUB_UNSAFE_OPENAPI_URL", "OpenAPI base URL must resolve only to public addresses.");
440
440
  }
441
441
  url.pathname = url.pathname.replace(/\/+$/, "");
@@ -444,10 +444,10 @@ async function normalizeOpenApiBaseUrl(value) {
444
444
  function defaultConfig() {
445
445
  return { version: 1, default_profile: "default", profiles: {} };
446
446
  }
447
- function parseConfig(text) {
447
+ function parseConfig(text2) {
448
448
  let parsed;
449
449
  try {
450
- parsed = parse(text);
450
+ parsed = parse(text2);
451
451
  } catch (error) {
452
452
  throw new AiHubError("AI_HUB_CONFIG_PARSE_ERROR", `Cannot parse config.toml: ${error instanceof Error ? error.message : "unknown error"}`);
453
453
  }
@@ -570,11 +570,11 @@ function strictObject(input, allowedKeys) {
570
570
  if (!input || typeof input !== "object" || Array.isArray(input)) {
571
571
  throw new AiHubError("AI_HUB_INVALID_ARGUMENT", "Tool input must be an object.");
572
572
  }
573
- const record = input;
574
- for (const key of Object.keys(record)) {
573
+ const record2 = input;
574
+ for (const key of Object.keys(record2)) {
575
575
  if (!allowedKeys.includes(key)) throw new AiHubError("AI_HUB_INVALID_ARGUMENT", `Unknown argument "${key}".`);
576
576
  }
577
- return record;
577
+ return record2;
578
578
  }
579
579
  function requiredString(input, name) {
580
580
  const value = input[name];
@@ -866,6 +866,246 @@ var assetTools = [
866
866
  }
867
867
  ];
868
868
 
869
+ // ../core/src/tools/market-summaries.ts
870
+ function record(value, message) {
871
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
872
+ throw new AiHubError("AI_HUB_OPENAPI_INVALID_RESPONSE", message);
873
+ }
874
+ return value;
875
+ }
876
+ function records(value, message) {
877
+ if (!Array.isArray(value) || value.some((item) => !item || typeof item !== "object" || Array.isArray(item))) {
878
+ throw new AiHubError("AI_HUB_OPENAPI_INVALID_RESPONSE", message);
879
+ }
880
+ return value;
881
+ }
882
+ function text(value) {
883
+ return value === void 0 || value === null ? null : String(value);
884
+ }
885
+ function finiteNumber(value) {
886
+ const parsed = typeof value === "number" ? value : Number(value);
887
+ return Number.isFinite(parsed) ? parsed : null;
888
+ }
889
+ function parseDecimal(value) {
890
+ const match = /^([+-]?)(\d+)(?:\.(\d+))?$/.exec(value);
891
+ return match ? { sign: match[1] === "-" ? -1n : 1n, whole: match[2] ?? "0", fraction: match[3] ?? "" } : null;
892
+ }
893
+ function formatScaledDecimal(value, scale) {
894
+ const sign = value < 0n ? "-" : "";
895
+ const absolute = (value < 0n ? -value : value).toString().padStart(scale + 1, "0");
896
+ if (!scale) return `${sign}${absolute}`;
897
+ const whole = absolute.slice(0, -scale);
898
+ const fraction = absolute.slice(-scale).replace(/0+$/, "");
899
+ return fraction ? `${sign}${whole}.${fraction}` : `${sign}${whole}`;
900
+ }
901
+ function sumDecimalStrings(values) {
902
+ const parsed = values.map(parseDecimal).filter((value) => value !== null);
903
+ if (!parsed.length) return "0";
904
+ const scale = Math.max(...parsed.map((value) => value.fraction.length));
905
+ const sum = parsed.reduce((total, value) => total + value.sign * BigInt(`${value.whole}${value.fraction.padEnd(scale, "0")}`), 0n);
906
+ return formatScaledDecimal(sum, scale);
907
+ }
908
+ function subtractDecimalStrings(left, right) {
909
+ const leftValue = parseDecimal(left);
910
+ const rightValue = parseDecimal(right);
911
+ if (!leftValue || !rightValue) return null;
912
+ const scale = Math.max(leftValue.fraction.length, rightValue.fraction.length);
913
+ const toScaled = (value) => value.sign * BigInt(`${value.whole}${value.fraction.padEnd(scale, "0")}`);
914
+ return formatScaledDecimal(toScaled(leftValue) - toScaled(rightValue), scale);
915
+ }
916
+ function numericMaximum(values, field) {
917
+ const numbers = values.map((item) => finiteNumber(item[field])).filter((item) => item !== null);
918
+ return numbers.length ? String(Math.max(...numbers)) : null;
919
+ }
920
+ function numericMinimum(values, field) {
921
+ const numbers = values.map((item) => finiteNumber(item[field])).filter((item) => item !== null);
922
+ return numbers.length ? String(Math.min(...numbers)) : null;
923
+ }
924
+ function compareByNumericField(field, direction) {
925
+ return (left, right) => {
926
+ const leftValue = finiteNumber(left[field]);
927
+ const rightValue = finiteNumber(right[field]);
928
+ if (leftValue === null && rightValue === null) return 0;
929
+ if (leftValue === null) return 1;
930
+ if (rightValue === null) return -1;
931
+ return direction === "asc" ? leftValue - rightValue : rightValue - leftValue;
932
+ };
933
+ }
934
+ function tickerItem(value) {
935
+ return {
936
+ symbol: text(value.symbol),
937
+ last: text(value.last),
938
+ rose: text(value.rose),
939
+ amount: text(value.amount),
940
+ vol: text(value.vol),
941
+ high: text(value.high),
942
+ low: text(value.low),
943
+ time: text(value.time)
944
+ };
945
+ }
946
+ function tickerRows(value) {
947
+ if (Array.isArray(value)) return records(value, "Ticker response must be an array.");
948
+ const payload = record(value, "Ticker response must be an array.");
949
+ if (Array.isArray(payload.items)) return records(payload.items, "Ticker response items must be an array.");
950
+ throw new AiHubError("AI_HUB_OPENAPI_INVALID_RESPONSE", "Ticker response must be an array.");
951
+ }
952
+ function quoteFromSymbol(symbol) {
953
+ if (!symbol) return null;
954
+ const parts = symbol.split("/");
955
+ return parts.length === 2 ? parts[1]?.toUpperCase() ?? null : null;
956
+ }
957
+ function summarizeTickers(value, options) {
958
+ const quoteAsset = options.quoteAsset.toUpperCase();
959
+ const rows2 = tickerRows(value).filter((item) => quoteFromSymbol(text(item.symbol)) === quoteAsset);
960
+ const bySymbol = new Map(rows2.map((item) => [text(item.symbol)?.toUpperCase(), item]));
961
+ const watchlistSymbols = ["BTC", "ETH", "SOL", "XRP", "DOGE", "BNB", "ADA"];
962
+ const watchlist = watchlistSymbols.map((asset) => bySymbol.get(`${asset}/${quoteAsset}`)).filter((item) => Boolean(item)).map(tickerItem);
963
+ const limit = options.limit;
964
+ return {
965
+ quoteAsset,
966
+ totalSymbols: rows2.length,
967
+ watchlist,
968
+ topGainers: [...rows2].sort(compareByNumericField("rose", "desc")).slice(0, limit).map(tickerItem),
969
+ topLosers: [...rows2].sort(compareByNumericField("rose", "asc")).slice(0, limit).map(tickerItem),
970
+ topByQuoteVolume: [...rows2].sort(compareByNumericField("amount", "desc")).slice(0, limit).map(tickerItem)
971
+ };
972
+ }
973
+ function summarizeSymbols(value, options) {
974
+ const payload = record(value, "Symbols response must be an object.");
975
+ const rows2 = records(payload.symbols, "Symbols response must contain a symbols array.");
976
+ const query = options.query?.trim().toUpperCase();
977
+ const quoteAsset = options.quoteAsset?.trim().toUpperCase();
978
+ const matches = rows2.filter((item) => {
979
+ const symbol = text(item.SymbolName) ?? text(item.symbol) ?? "";
980
+ const quote = text(item.quoteAsset)?.toUpperCase();
981
+ return (!query || symbol.toUpperCase().includes(query)) && (!quoteAsset || quote === quoteAsset);
982
+ });
983
+ const sortedMatches = [...matches].sort((left, right) => {
984
+ if (!query) return 0;
985
+ const rank = (item) => {
986
+ const symbol = (text(item.SymbolName) ?? text(item.symbol) ?? "").toUpperCase();
987
+ return symbol.startsWith(`${query}/`) ? 0 : symbol.startsWith(query) ? 1 : 2;
988
+ };
989
+ return rank(left) - rank(right);
990
+ });
991
+ const quoteAssetCounts = /* @__PURE__ */ new Map();
992
+ for (const item of rows2) {
993
+ const quote = text(item.quoteAsset)?.toUpperCase();
994
+ if (quote) quoteAssetCounts.set(quote, (quoteAssetCounts.get(quote) ?? 0) + 1);
995
+ }
996
+ const items = sortedMatches.slice(0, options.limit).map((item) => ({
997
+ symbol: text(item.SymbolName) ?? text(item.symbol),
998
+ apiSymbol: text(item.symbol),
999
+ baseAsset: text(item.baseAssetName) ?? text(item.baseAsset),
1000
+ quoteAsset: text(item.quoteAssetName) ?? text(item.quoteAsset),
1001
+ pricePrecision: item.pricePrecision ?? null,
1002
+ quantityPrecision: item.quantityPrecision ?? null,
1003
+ limitVolumeMin: text(item.limitVolumeMin),
1004
+ limitPriceMin: text(item.limitPriceMin),
1005
+ limitAmountMin: text(item.limitAmountMin)
1006
+ }));
1007
+ return {
1008
+ totalSymbols: rows2.length,
1009
+ matchedSymbols: matches.length,
1010
+ quoteAssetCounts: [...quoteAssetCounts.entries()].sort((left, right) => right[1] - left[1]).map(([asset, count]) => ({ asset, count })),
1011
+ items
1012
+ };
1013
+ }
1014
+ function level(value) {
1015
+ if (!Array.isArray(value)) return { price: null, quantity: null };
1016
+ return { price: text(value[0]), quantity: text(value[1]) };
1017
+ }
1018
+ function summarizeDepth(value, symbol) {
1019
+ const payload = record(value, "Depth response must be an object.");
1020
+ const bids = Array.isArray(payload.bids) ? payload.bids : [];
1021
+ const asks = Array.isArray(payload.asks) ? payload.asks : [];
1022
+ const bestBid = level(bids[0]);
1023
+ const bestAsk = level(asks[0]);
1024
+ const bidPrice = finiteNumber(bestBid.price);
1025
+ const askPrice = finiteNumber(bestAsk.price);
1026
+ const spread = typeof bestBid.price !== "string" || typeof bestAsk.price !== "string" ? null : subtractDecimalStrings(bestAsk.price, bestBid.price);
1027
+ const midPrice = bidPrice === null || askPrice === null ? null : (askPrice + bidPrice) / 2;
1028
+ return {
1029
+ symbol,
1030
+ time: text(payload.time),
1031
+ bestBid,
1032
+ bestAsk,
1033
+ spread,
1034
+ spreadRatio: spread === null || !midPrice ? null : String(Number(spread) / midPrice),
1035
+ bids: bids.map(level),
1036
+ asks: asks.map(level)
1037
+ };
1038
+ }
1039
+ function summarizeTrades(value, symbol) {
1040
+ const payload = record(value, "Trades response must be an object.");
1041
+ const rows2 = records(payload.list, "Trades response must contain a list array.");
1042
+ let buyCount = 0;
1043
+ let sellCount = 0;
1044
+ const buyQuantities = [];
1045
+ const sellQuantities = [];
1046
+ for (const item of rows2) {
1047
+ const quantity = text(item.qty);
1048
+ if (text(item.side)?.toUpperCase() === "BUY") {
1049
+ buyCount += 1;
1050
+ if (quantity) buyQuantities.push(quantity);
1051
+ } else if (text(item.side)?.toUpperCase() === "SELL") {
1052
+ sellCount += 1;
1053
+ if (quantity) sellQuantities.push(quantity);
1054
+ }
1055
+ }
1056
+ return {
1057
+ symbol,
1058
+ count: rows2.length,
1059
+ lastPrice: text(rows2[0]?.price),
1060
+ highPrice: numericMaximum(rows2, "price"),
1061
+ lowPrice: numericMinimum(rows2, "price"),
1062
+ buyCount,
1063
+ sellCount,
1064
+ buyQuantity: sumDecimalStrings(buyQuantities),
1065
+ sellQuantity: sumDecimalStrings(sellQuantities),
1066
+ recentTrades: rows2.map((item) => ({
1067
+ id: text(item.id),
1068
+ side: text(item.side),
1069
+ price: text(item.price),
1070
+ quantity: text(item.qty),
1071
+ time: text(item.time)
1072
+ }))
1073
+ };
1074
+ }
1075
+ function summarizeKlines(value, symbol, interval) {
1076
+ const rows2 = records(value, "Klines response must be an array.");
1077
+ const newest = rows2[0];
1078
+ const oldest = rows2[rows2.length - 1];
1079
+ const oldestOpen = finiteNumber(oldest?.open);
1080
+ const newestClose = finiteNumber(newest?.close);
1081
+ return {
1082
+ symbol,
1083
+ interval,
1084
+ count: rows2.length,
1085
+ periodOpen: text(oldest?.open),
1086
+ latestClose: text(newest?.close),
1087
+ changeRatio: oldestOpen === null || !oldestOpen || newestClose === null ? null : String((newestClose - oldestOpen) / oldestOpen),
1088
+ high: numericMaximum(rows2, "high"),
1089
+ low: numericMinimum(rows2, "low"),
1090
+ latestCandle: newest ? {
1091
+ time: text(newest.idx),
1092
+ open: text(newest.open),
1093
+ close: text(newest.close),
1094
+ high: text(newest.high),
1095
+ low: text(newest.low),
1096
+ volume: text(newest.vol)
1097
+ } : null,
1098
+ candles: rows2.map((item) => ({
1099
+ time: text(item.idx),
1100
+ open: text(item.open),
1101
+ close: text(item.close),
1102
+ high: text(item.high),
1103
+ low: text(item.low),
1104
+ volume: text(item.vol)
1105
+ }))
1106
+ };
1107
+ }
1108
+
869
1109
  // ../core/src/tools/market-tools.ts
870
1110
  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"];
871
1111
  var marketTools = [
@@ -903,7 +1143,7 @@ var marketTools = [
903
1143
  {
904
1144
  name: "market_get_symbols",
905
1145
  title: "Get Spot Symbols",
906
- description: "Get spot symbols from the configured tenant OpenAPI.",
1146
+ 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.",
907
1147
  cliPath: ["market", "symbols"],
908
1148
  module: "spot-common",
909
1149
  access: "public",
@@ -914,10 +1154,30 @@ var marketTools = [
914
1154
  validate: (input) => strictObject(input, []),
915
1155
  handler: (_input, context) => context.api.symbols()
916
1156
  },
1157
+ {
1158
+ name: "market_search_symbols",
1159
+ 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.",
1161
+ cliPath: ["market", "symbols-search"],
1162
+ module: "spot-common",
1163
+ access: "public",
1164
+ operation: "read",
1165
+ riskLevel: "low",
1166
+ inputSchema: { type: "object", properties: { query: { type: "string" }, quoteAsset: { type: "string" }, limit: { type: "integer", minimum: 1, maximum: 50 } }, additionalProperties: false },
1167
+ errorCodes: readErrors,
1168
+ validate: (input) => {
1169
+ const value = strictObject(input, ["query", "quoteAsset", "limit"]);
1170
+ return { query: optionalString(value, "query"), quoteAsset: optionalString(value, "quoteAsset"), limit: optionalInteger(value, "limit", 20, 1, 50) };
1171
+ },
1172
+ handler: async (input, context) => {
1173
+ const value = input;
1174
+ return summarizeSymbols(await context.api.symbols(), value);
1175
+ }
1176
+ },
917
1177
  {
918
1178
  name: "market_get_ticker",
919
1179
  title: "Get Spot Ticker",
920
- description: "Get spot ticker data. Pass symbol or symbols when filtering is needed. The raw OpenAPI response is a ticker array.",
1180
+ description: "Get exact raw spot ticker data for one symbol or an explicitly requested symbol list. For an all-market overview, use market_get_ticker_summary instead.",
921
1181
  cliPath: ["market", "ticker"],
922
1182
  module: "spot-common",
923
1183
  access: "public",
@@ -927,10 +1187,33 @@ var marketTools = [
927
1187
  errorCodes: readErrors,
928
1188
  validate: (input) => {
929
1189
  const value = strictObject(input, ["symbol", "symbols", "timeZone"]);
930
- return { symbol: optionalString(value, "symbol"), symbols: optionalString(value, "symbols"), timeZone: optionalString(value, "timeZone") };
1190
+ const symbol = optionalString(value, "symbol");
1191
+ const symbols = optionalString(value, "symbols");
1192
+ if (!symbol && !symbols) throw new AiHubError("AI_HUB_INVALID_ARGUMENT", "market_get_ticker requires symbol or symbols. Use market_get_ticker_summary for all-market data.");
1193
+ return { symbol, symbols, timeZone: optionalString(value, "timeZone") };
931
1194
  },
932
1195
  handler: (input, context) => context.api.ticker(input)
933
1196
  },
1197
+ {
1198
+ name: "market_get_ticker_summary",
1199
+ title: "Get Spot Market Summary",
1200
+ description: "Return a bounded all-market overview: a core-asset watchlist plus gainers, losers, and quote-volume leaders. Use this for requests such as market ticker, market overview, movers, or most-active symbols.",
1201
+ cliPath: ["market", "ticker-summary"],
1202
+ module: "spot-common",
1203
+ access: "public",
1204
+ operation: "read",
1205
+ riskLevel: "low",
1206
+ inputSchema: { type: "object", properties: { quoteAsset: { type: "string" }, limit: { type: "integer", minimum: 1, maximum: 20 } }, additionalProperties: false },
1207
+ errorCodes: readErrors,
1208
+ validate: (input) => {
1209
+ const value = strictObject(input, ["quoteAsset", "limit"]);
1210
+ return { quoteAsset: optionalString(value, "quoteAsset") ?? "USDT", limit: optionalInteger(value, "limit", 5, 1, 20) };
1211
+ },
1212
+ handler: async (input, context) => {
1213
+ const value = input;
1214
+ return summarizeTickers(await context.api.ticker(), value);
1215
+ }
1216
+ },
934
1217
  {
935
1218
  name: "market_get_depth",
936
1219
  title: "Get Spot Depth",
@@ -951,6 +1234,26 @@ var marketTools = [
951
1234
  return context.api.depth(value.symbol, value.limit);
952
1235
  }
953
1236
  },
1237
+ {
1238
+ name: "market_get_depth_summary",
1239
+ title: "Get Spot Order Book Summary",
1240
+ description: "Get a bounded order-book view with best bid, best ask, spread, and requested levels. Prefer this to market_get_depth for Agent analysis.",
1241
+ cliPath: ["market", "depth-summary"],
1242
+ module: "spot-common",
1243
+ access: "public",
1244
+ operation: "read",
1245
+ riskLevel: "low",
1246
+ inputSchema: { type: "object", properties: { symbol: { type: "string" }, limit: { type: "integer", minimum: 1, maximum: 20 } }, required: ["symbol"], additionalProperties: false },
1247
+ errorCodes: readErrors,
1248
+ validate: (input) => {
1249
+ const value = strictObject(input, ["symbol", "limit"]);
1250
+ return { symbol: requiredString(value, "symbol"), limit: optionalInteger(value, "limit", 10, 1, 20) };
1251
+ },
1252
+ handler: async (input, context) => {
1253
+ const value = input;
1254
+ return summarizeDepth(await context.api.depth(value.symbol, value.limit), value.symbol);
1255
+ }
1256
+ },
954
1257
  {
955
1258
  name: "market_get_trades",
956
1259
  title: "Get Recent Spot Trades",
@@ -960,17 +1263,37 @@ var marketTools = [
960
1263
  access: "public",
961
1264
  operation: "read",
962
1265
  riskLevel: "low",
963
- inputSchema: { type: "object", properties: { symbol: { type: "string" }, limit: { type: "integer", minimum: 1, maximum: 1e3 } }, required: ["symbol"], additionalProperties: false },
1266
+ inputSchema: { type: "object", properties: { symbol: { type: "string" }, limit: { type: "integer", minimum: 1, maximum: 100 } }, required: ["symbol"], additionalProperties: false },
964
1267
  errorCodes: readErrors,
965
1268
  validate: (input) => {
966
1269
  const value = strictObject(input, ["symbol", "limit"]);
967
- return { symbol: requiredString(value, "symbol"), limit: optionalInteger(value, "limit", 100, 1, 1e3) };
1270
+ return { symbol: requiredString(value, "symbol"), limit: optionalInteger(value, "limit", 20, 1, 100) };
968
1271
  },
969
1272
  handler: (input, context) => {
970
1273
  const value = input;
971
1274
  return context.api.trades(value.symbol, value.limit);
972
1275
  }
973
1276
  },
1277
+ {
1278
+ name: "market_get_trades_summary",
1279
+ title: "Get Recent Spot Trades Summary",
1280
+ description: "Get a bounded recent-trades view with price range, buy/sell counts and quantities, plus the requested recent sample. Prefer this to market_get_trades for Agent analysis.",
1281
+ cliPath: ["market", "trades-summary"],
1282
+ module: "spot-common",
1283
+ access: "public",
1284
+ operation: "read",
1285
+ riskLevel: "low",
1286
+ inputSchema: { type: "object", properties: { symbol: { type: "string" }, limit: { type: "integer", minimum: 1, maximum: 50 } }, required: ["symbol"], additionalProperties: false },
1287
+ errorCodes: readErrors,
1288
+ validate: (input) => {
1289
+ const value = strictObject(input, ["symbol", "limit"]);
1290
+ return { symbol: requiredString(value, "symbol"), limit: optionalInteger(value, "limit", 20, 1, 50) };
1291
+ },
1292
+ handler: async (input, context) => {
1293
+ const value = input;
1294
+ return summarizeTrades(await context.api.trades(value.symbol, value.limit), value.symbol);
1295
+ }
1296
+ },
974
1297
  {
975
1298
  name: "market_get_klines",
976
1299
  title: "Get Spot Klines",
@@ -980,15 +1303,37 @@ var marketTools = [
980
1303
  access: "public",
981
1304
  operation: "read",
982
1305
  riskLevel: "low",
983
- 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: 1e3 } }, required: ["symbol", "interval"], additionalProperties: false },
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: 100 } }, required: ["symbol", "interval"], additionalProperties: false },
984
1307
  errorCodes: readErrors,
985
1308
  validate: (input) => {
986
1309
  const value = strictObject(input, ["symbol", "interval", "startTime", "endTime", "timezone", "limit"]);
987
1310
  const startTime = value.startTime === void 0 ? void 0 : optionalInteger(value, "startTime", 0, 0, Number.MAX_SAFE_INTEGER);
988
1311
  const endTime = value.endTime === void 0 ? void 0 : optionalInteger(value, "endTime", 0, 0, Number.MAX_SAFE_INTEGER);
989
- return { symbol: requiredString(value, "symbol"), interval: requiredString(value, "interval"), startTime, endTime, timezone: optionalString(value, "timezone"), limit: optionalInteger(value, "limit", 100, 1, 1e3) };
1312
+ return { symbol: requiredString(value, "symbol"), interval: requiredString(value, "interval"), startTime, endTime, timezone: optionalString(value, "timezone"), limit: optionalInteger(value, "limit", 50, 1, 100) };
990
1313
  },
991
1314
  handler: (input, context) => context.api.klines(input)
1315
+ },
1316
+ {
1317
+ name: "market_get_klines_summary",
1318
+ 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.",
1320
+ cliPath: ["market", "klines-summary"],
1321
+ module: "spot-common",
1322
+ access: "public",
1323
+ operation: "read",
1324
+ 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", "interval"], additionalProperties: false },
1326
+ errorCodes: readErrors,
1327
+ validate: (input) => {
1328
+ const value = strictObject(input, ["symbol", "interval", "startTime", "endTime", "timezone", "limit"]);
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) };
1332
+ },
1333
+ handler: async (input, context) => {
1334
+ const value = input;
1335
+ return summarizeKlines(await context.api.klines(value), value.symbol, value.interval);
1336
+ }
992
1337
  }
993
1338
  ];
994
1339
 
@@ -2126,16 +2471,36 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2126
2471
  import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
2127
2472
  var CAPABILITIES_TOOL = "system_get_capabilities";
2128
2473
  var CONFIRM_ACTION_TOOL = "confirm_action";
2474
+ var READ_RESULT_OUTPUT_SCHEMA = {
2475
+ type: "object",
2476
+ properties: {
2477
+ ok: { type: "boolean", const: true },
2478
+ data: {
2479
+ type: "object",
2480
+ properties: {
2481
+ dataType: { type: "string", enum: ["array", "object", "scalar", "null"] },
2482
+ items: { type: "array" },
2483
+ count: { type: "integer", minimum: 0 },
2484
+ value: {}
2485
+ },
2486
+ required: ["dataType"]
2487
+ }
2488
+ },
2489
+ required: ["ok", "data"]
2490
+ };
2129
2491
  function result(data) {
2130
2492
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
2131
2493
  }
2132
- function formatMcpData(toolName, data) {
2494
+ function toMcpReadResult(data) {
2495
+ const payload = { ok: true, data };
2496
+ return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }], structuredContent: payload };
2497
+ }
2498
+ function formatMcpData(data) {
2133
2499
  if (Array.isArray(data)) {
2134
2500
  return {
2135
2501
  dataType: "array",
2136
2502
  items: data,
2137
- count: data.length,
2138
- ...toolName === "market_get_ticker" ? { tickers: data } : {}
2503
+ count: data.length
2139
2504
  };
2140
2505
  }
2141
2506
  if (data === null || data === void 0) return { dataType: "null", value: null };
@@ -2150,8 +2515,9 @@ function toMcpTool(tool) {
2150
2515
  return {
2151
2516
  name: tool.name,
2152
2517
  title: tool.title,
2153
- description: `${tool.description}${tool.operation === "read" ? " Successful MCP output is always { ok: true, data: ... }. For list results, use data.items and data.count (market_get_ticker also provides data.tickers); for object results, use data.value. Check data.dataType before formatting." : ""}`,
2518
+ description: `${tool.description}${tool.operation === "read" ? " Successful MCP output is structured as { ok: true, data: ... }. For list results, use data.items and data.count; for object results, use data.value. Check data.dataType before formatting." : ""}`,
2154
2519
  inputSchema: tool.inputSchema,
2520
+ ...tool.operation === "read" ? { outputSchema: READ_RESULT_OUTPUT_SCHEMA } : {},
2155
2521
  annotations: {
2156
2522
  readOnlyHint: tool.operation === "read",
2157
2523
  destructiveHint: tool.riskLevel === "high",
@@ -2177,10 +2543,10 @@ function createServer(profileName, readOnly) {
2177
2543
  const registry = createToolRegistry();
2178
2544
  const writeExecutor = new ToolWriteExecutor(registry);
2179
2545
  const server = new Server(
2180
- { name: "ai-hub-agent-trade", version: "0.1.4" },
2546
+ { name: "ai-hub-agent-trade", version: "0.1.6" },
2181
2547
  {
2182
2548
  capabilities: { tools: {} },
2183
- instructions: "Every successful tool response is JSON text 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. market_get_ticker also provides data.tickers as an alias of data.items. Inspect data.dataType before formatting and never assume undocumented nested keys. 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."
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 market_search_symbols, 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."
2184
2550
  }
2185
2551
  );
2186
2552
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
@@ -2227,7 +2593,7 @@ function createServer(profileName, readOnly) {
2227
2593
  return result({ ok: true, data: await writeExecutor.prepare(preparedTool.name, request.params.arguments ?? {}, context) });
2228
2594
  }
2229
2595
  const data = await registry.execute(request.params.name, request.params.arguments ?? {}, context, { readOnly });
2230
- return result({ ok: true, data: formatMcpData(request.params.name, data) });
2596
+ return toMcpReadResult(formatMcpData(data));
2231
2597
  } catch (error) {
2232
2598
  return toMcpErrorResult(error);
2233
2599
  }
package/package.json CHANGED
@@ -1,8 +1,13 @@
1
1
  {
2
2
  "name": "@aihubspot/agent-trade-mcp",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "Local stdio MCP server for AI Hub spot trading tools",
5
5
  "license": "UNLICENSED",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/exhcange/ai-hub-agent-trade",
9
+ "directory": "packages/mcp"
10
+ },
6
11
  "type": "module",
7
12
  "main": "./dist/index.js",
8
13
  "types": "./dist/index.d.ts",