@aihubspot/agent-trade-mcp 0.1.5 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +14 -2
  2. package/dist/index.js +552 -144
  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. Use this for every MCP symbol lookup; it returns matching rows in `data.value.items`.
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. It defaults to `60min`; formal intervals are `1min`, `5min`, `15min`, `30min`, `60min`, `1day`, `1week`, and `1month`.
47
+
48
+ The complete raw symbols endpoint is intentionally CLI-only because its payload is too large for Agent contexts. Raw ticker, depth, trades, and kline tools remain available only when an exact payload is explicitly needed. Raw trades are capped at 100 rows; raw klines support the upstream maximum of 300 rows.
package/dist/index.js CHANGED
@@ -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,134 +866,248 @@ var assetTools = [
866
866
  }
867
867
  ];
868
868
 
869
- // ../core/src/tools/market-tools.ts
870
- 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
- var marketTools = [
872
- {
873
- name: "market_ping",
874
- title: "Test OpenAPI Connection",
875
- description: "Test the configured tenant OpenAPI connection.",
876
- cliPath: ["market", "ping"],
877
- module: "spot-common",
878
- access: "public",
879
- operation: "read",
880
- riskLevel: "low",
881
- inputSchema: { type: "object", additionalProperties: false },
882
- errorCodes: ["AI_HUB_OPENAPI_NETWORK_ERROR", "AI_HUB_OPENAPI_HTTP_ERROR", "AI_HUB_OPENAPI_INVALID_RESPONSE", "AI_HUB_OPENAPI_BUSINESS_ERROR"],
883
- validate: (input) => {
884
- strictObject(input, []);
885
- return {};
886
- },
887
- handler: (_input, context) => context.api.ping()
888
- },
889
- {
890
- name: "market_get_server_time",
891
- title: "Get Server Time",
892
- description: "Get server time from the configured tenant OpenAPI.",
893
- cliPath: ["market", "time"],
894
- module: "spot-common",
895
- access: "public",
896
- operation: "read",
897
- riskLevel: "low",
898
- inputSchema: { type: "object", additionalProperties: false },
899
- errorCodes: readErrors,
900
- validate: (input) => strictObject(input, []),
901
- handler: (_input, context) => context.api.time()
902
- },
903
- {
904
- name: "market_get_symbols",
905
- title: "Get Spot Symbols",
906
- description: "Get spot symbols from the configured tenant OpenAPI.",
907
- cliPath: ["market", "symbols"],
908
- module: "spot-common",
909
- access: "public",
910
- operation: "read",
911
- riskLevel: "low",
912
- inputSchema: { type: "object", additionalProperties: false },
913
- errorCodes: readErrors,
914
- validate: (input) => strictObject(input, []),
915
- handler: (_input, context) => context.api.symbols()
916
- },
917
- {
918
- name: "market_get_ticker",
919
- 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.",
921
- cliPath: ["market", "ticker"],
922
- module: "spot-common",
923
- access: "public",
924
- operation: "read",
925
- riskLevel: "low",
926
- inputSchema: { type: "object", properties: { symbol: { type: "string" }, symbols: { type: "string" }, timeZone: { type: "string" } }, additionalProperties: false },
927
- errorCodes: readErrors,
928
- validate: (input) => {
929
- const value = strictObject(input, ["symbol", "symbols", "timeZone"]);
930
- return { symbol: optionalString(value, "symbol"), symbols: optionalString(value, "symbols"), timeZone: optionalString(value, "timeZone") };
931
- },
932
- handler: (input, context) => context.api.ticker(input)
933
- },
934
- {
935
- name: "market_get_depth",
936
- title: "Get Spot Depth",
937
- description: "Get the spot order book for one symbol.",
938
- cliPath: ["market", "depth"],
939
- module: "spot-common",
940
- access: "public",
941
- operation: "read",
942
- riskLevel: "low",
943
- inputSchema: { type: "object", properties: { symbol: { type: "string" }, limit: { type: "integer", minimum: 1, maximum: 100 } }, required: ["symbol"], additionalProperties: false },
944
- errorCodes: readErrors,
945
- validate: (input) => {
946
- const value = strictObject(input, ["symbol", "limit"]);
947
- return { symbol: requiredString(value, "symbol"), limit: optionalInteger(value, "limit", 20, 1, 100) };
948
- },
949
- handler: (input, context) => {
950
- const value = input;
951
- return context.api.depth(value.symbol, value.limit);
952
- }
953
- },
954
- {
955
- name: "market_get_trades",
956
- title: "Get Recent Spot Trades",
957
- description: "Get recent spot trades for one symbol.",
958
- cliPath: ["market", "trades"],
959
- module: "spot-common",
960
- access: "public",
961
- operation: "read",
962
- riskLevel: "low",
963
- inputSchema: { type: "object", properties: { symbol: { type: "string" }, limit: { type: "integer", minimum: 1, maximum: 1e3 } }, required: ["symbol"], additionalProperties: false },
964
- errorCodes: readErrors,
965
- validate: (input) => {
966
- const value = strictObject(input, ["symbol", "limit"]);
967
- return { symbol: requiredString(value, "symbol"), limit: optionalInteger(value, "limit", 100, 1, 1e3) };
968
- },
969
- handler: (input, context) => {
970
- const value = input;
971
- return context.api.trades(value.symbol, value.limit);
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);
972
1054
  }
973
- },
974
- {
975
- name: "market_get_klines",
976
- title: "Get Spot Klines",
977
- description: "Get spot candlestick data for one symbol and interval.",
978
- cliPath: ["market", "klines"],
979
- module: "spot-common",
980
- access: "public",
981
- operation: "read",
982
- 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 },
984
- errorCodes: readErrors,
985
- validate: (input) => {
986
- const value = strictObject(input, ["symbol", "interval", "startTime", "endTime", "timezone", "limit"]);
987
- const startTime = value.startTime === void 0 ? void 0 : optionalInteger(value, "startTime", 0, 0, Number.MAX_SAFE_INTEGER);
988
- 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) };
990
- },
991
- handler: (input, context) => context.api.klines(input)
992
1055
  }
993
- ];
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
+ }
994
1108
 
995
1109
  // ../core/src/tools/symbol-rules.ts
996
- var CACHE_TTL_MS = 5 * 60 * 1e3;
1110
+ var CACHE_TTL_MS = 60 * 60 * 1e3;
997
1111
  var cache = /* @__PURE__ */ new Map();
998
1112
  var pendingLoads = /* @__PURE__ */ new Map();
999
1113
  function cacheKey(context) {
@@ -1037,22 +1151,25 @@ function parseRules(response) {
1037
1151
  }
1038
1152
  return result2;
1039
1153
  }
1040
- async function loadRules(context) {
1154
+ async function loadSnapshot(context) {
1041
1155
  const key = cacheKey(context);
1042
1156
  const current = cache.get(key);
1043
1157
  if (current && current.expiresAt > Date.now()) return current;
1044
1158
  const pending = pendingLoads.get(key);
1045
1159
  if (pending) return pending;
1046
1160
  const load = context.api.symbols().then((response) => {
1047
- const entry = { rules: parseRules(response), expiresAt: Date.now() + CACHE_TTL_MS };
1161
+ const entry = { response, rules: parseRules(response), expiresAt: Date.now() + CACHE_TTL_MS };
1048
1162
  cache.set(key, entry);
1049
1163
  return entry;
1050
1164
  }).finally(() => pendingLoads.delete(key));
1051
1165
  pendingLoads.set(key, load);
1052
1166
  return load;
1053
1167
  }
1168
+ async function getCachedSymbols(context) {
1169
+ return (await loadSnapshot(context)).response;
1170
+ }
1054
1171
  async function getSymbolRule(context, symbol) {
1055
- const entry = await loadRules(context);
1172
+ const entry = await loadSnapshot(context);
1056
1173
  const rule = entry.rules.get(normalizedSymbol(symbol));
1057
1174
  if (!rule) throw new AiHubError("AI_HUB_SYMBOL_NOT_FOUND", `Symbol "${symbol}" was not returned by the configured tenant OpenAPI symbols endpoint.`);
1058
1175
  return rule;
@@ -1108,6 +1225,269 @@ async function preflightSymbolOrder(context, order) {
1108
1225
  return rule;
1109
1226
  }
1110
1227
 
1228
+ // ../core/src/tools/market-tools.ts
1229
+ var readErrors = ["AI_HUB_INVALID_ARGUMENT", "AI_HUB_OPENAPI_NETWORK_ERROR", "AI_HUB_OPENAPI_HTTP_ERROR", "AI_HUB_OPENAPI_INVALID_RESPONSE", "AI_HUB_OPENAPI_BUSINESS_ERROR"];
1230
+ var KLINE_INTERVALS = ["1min", "5min", "15min", "30min", "60min", "1day", "1week", "1month"];
1231
+ var KLINE_INTERVAL_ALIASES = {
1232
+ "1m": "1min",
1233
+ "5m": "5min",
1234
+ "15m": "15min",
1235
+ "30m": "30min",
1236
+ "60m": "60min",
1237
+ "1h": "60min",
1238
+ "1d": "1day",
1239
+ "1w": "1week",
1240
+ "1mo": "1month",
1241
+ "1M": "1month"
1242
+ };
1243
+ var klineIntervalDescription = "Kline interval. Supported values: 1min, 5min, 15min, 30min, 60min, 1day, 1week, 1month. Use 60min, not 1h.";
1244
+ var klineTimezoneDescription = "Optional timezone such as UTC+08 or UTC-09. Defaults to UTC+08. The upstream API applies timezone-specific data only to periods above 60min.";
1245
+ function normalizeKlineInterval(value) {
1246
+ const trimmed = value.trim();
1247
+ const normalized = KLINE_INTERVAL_ALIASES[trimmed] ?? trimmed.toLowerCase();
1248
+ if (KLINE_INTERVALS.includes(normalized)) return normalized;
1249
+ throw new AiHubError("AI_HUB_INVALID_ARGUMENT", `interval must be one of: ${KLINE_INTERVALS.join(", ")}. Use 60min, not 1h.`);
1250
+ }
1251
+ function validateKlineInput(input, options) {
1252
+ const value = strictObject(input, ["symbol", "interval", "startTime", "endTime", "timezone", "limit"]);
1253
+ const startTime = value.startTime === void 0 ? void 0 : optionalInteger(value, "startTime", 0, 0, Number.MAX_SAFE_INTEGER);
1254
+ const endTime = value.endTime === void 0 ? void 0 : optionalInteger(value, "endTime", 0, 0, Number.MAX_SAFE_INTEGER);
1255
+ if (startTime !== void 0 && endTime !== void 0 && startTime > endTime) {
1256
+ throw new AiHubError("AI_HUB_INVALID_ARGUMENT", "startTime must be less than or equal to endTime.");
1257
+ }
1258
+ return {
1259
+ symbol: requiredString(value, "symbol"),
1260
+ interval: value.interval === void 0 && options.summary ? "60min" : normalizeKlineInterval(requiredString(value, "interval")),
1261
+ startTime,
1262
+ endTime,
1263
+ timezone: optionalString(value, "timezone"),
1264
+ limit: optionalInteger(value, "limit", options.summary ? 20 : 50, 1, options.summary ? 100 : 300)
1265
+ };
1266
+ }
1267
+ var marketTools = [
1268
+ {
1269
+ name: "market_ping",
1270
+ title: "Test OpenAPI Connection",
1271
+ description: "Test the configured tenant OpenAPI connection.",
1272
+ cliPath: ["market", "ping"],
1273
+ module: "spot-common",
1274
+ access: "public",
1275
+ operation: "read",
1276
+ riskLevel: "low",
1277
+ inputSchema: { type: "object", additionalProperties: false },
1278
+ errorCodes: ["AI_HUB_OPENAPI_NETWORK_ERROR", "AI_HUB_OPENAPI_HTTP_ERROR", "AI_HUB_OPENAPI_INVALID_RESPONSE", "AI_HUB_OPENAPI_BUSINESS_ERROR"],
1279
+ validate: (input) => {
1280
+ strictObject(input, []);
1281
+ return {};
1282
+ },
1283
+ handler: (_input, context) => context.api.ping()
1284
+ },
1285
+ {
1286
+ name: "market_get_server_time",
1287
+ title: "Get Server Time",
1288
+ description: "Get server time from the configured tenant OpenAPI.",
1289
+ cliPath: ["market", "time"],
1290
+ module: "spot-common",
1291
+ access: "public",
1292
+ operation: "read",
1293
+ riskLevel: "low",
1294
+ inputSchema: { type: "object", additionalProperties: false },
1295
+ errorCodes: readErrors,
1296
+ validate: (input) => strictObject(input, []),
1297
+ handler: (_input, context) => context.api.time()
1298
+ },
1299
+ {
1300
+ name: "market_get_symbols",
1301
+ title: "Get Spot Symbols",
1302
+ 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.",
1303
+ cliPath: ["market", "symbols"],
1304
+ module: "spot-common",
1305
+ access: "public",
1306
+ operation: "read",
1307
+ riskLevel: "low",
1308
+ mcpVisible: false,
1309
+ inputSchema: { type: "object", additionalProperties: false },
1310
+ errorCodes: readErrors,
1311
+ validate: (input) => strictObject(input, []),
1312
+ handler: (_input, context) => getCachedSymbols(context)
1313
+ },
1314
+ {
1315
+ name: "market_search_symbols",
1316
+ title: "Search Spot Symbols",
1317
+ description: "Search the configured tenant's spot symbols and return a bounded metadata result. In MCP, matching rows are at data.value.items. Use this instead of market_get_symbols unless complete raw metadata is explicitly required.",
1318
+ cliPath: ["market", "symbols-search"],
1319
+ module: "spot-common",
1320
+ access: "public",
1321
+ operation: "read",
1322
+ riskLevel: "low",
1323
+ inputSchema: { type: "object", properties: { query: { type: "string" }, quoteAsset: { type: "string" }, limit: { type: "integer", minimum: 1, maximum: 50 } }, additionalProperties: false },
1324
+ errorCodes: readErrors,
1325
+ validate: (input) => {
1326
+ const value = strictObject(input, ["query", "quoteAsset", "limit"]);
1327
+ return { query: optionalString(value, "query"), quoteAsset: optionalString(value, "quoteAsset"), limit: optionalInteger(value, "limit", 20, 1, 50) };
1328
+ },
1329
+ handler: async (input, context) => {
1330
+ const value = input;
1331
+ return summarizeSymbols(await getCachedSymbols(context), value);
1332
+ }
1333
+ },
1334
+ {
1335
+ name: "market_get_ticker",
1336
+ title: "Get Spot Ticker",
1337
+ 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.",
1338
+ cliPath: ["market", "ticker"],
1339
+ module: "spot-common",
1340
+ access: "public",
1341
+ operation: "read",
1342
+ riskLevel: "low",
1343
+ inputSchema: { type: "object", properties: { symbol: { type: "string" }, symbols: { type: "string" }, timeZone: { type: "string" } }, additionalProperties: false },
1344
+ errorCodes: readErrors,
1345
+ validate: (input) => {
1346
+ const value = strictObject(input, ["symbol", "symbols", "timeZone"]);
1347
+ const symbol = optionalString(value, "symbol");
1348
+ const symbols = optionalString(value, "symbols");
1349
+ 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.");
1350
+ return { symbol, symbols, timeZone: optionalString(value, "timeZone") };
1351
+ },
1352
+ handler: (input, context) => context.api.ticker(input)
1353
+ },
1354
+ {
1355
+ name: "market_get_ticker_summary",
1356
+ title: "Get Spot Market Summary",
1357
+ 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.",
1358
+ cliPath: ["market", "ticker-summary"],
1359
+ module: "spot-common",
1360
+ access: "public",
1361
+ operation: "read",
1362
+ riskLevel: "low",
1363
+ inputSchema: { type: "object", properties: { quoteAsset: { type: "string" }, limit: { type: "integer", minimum: 1, maximum: 20 } }, additionalProperties: false },
1364
+ errorCodes: readErrors,
1365
+ validate: (input) => {
1366
+ const value = strictObject(input, ["quoteAsset", "limit"]);
1367
+ return { quoteAsset: optionalString(value, "quoteAsset") ?? "USDT", limit: optionalInteger(value, "limit", 5, 1, 20) };
1368
+ },
1369
+ handler: async (input, context) => {
1370
+ const value = input;
1371
+ return summarizeTickers(await context.api.ticker(), value);
1372
+ }
1373
+ },
1374
+ {
1375
+ name: "market_get_depth",
1376
+ title: "Get Spot Depth",
1377
+ description: "Get the spot order book for one symbol.",
1378
+ cliPath: ["market", "depth"],
1379
+ module: "spot-common",
1380
+ access: "public",
1381
+ operation: "read",
1382
+ riskLevel: "low",
1383
+ inputSchema: { type: "object", properties: { symbol: { type: "string" }, limit: { type: "integer", minimum: 1, maximum: 100 } }, required: ["symbol"], additionalProperties: false },
1384
+ errorCodes: readErrors,
1385
+ validate: (input) => {
1386
+ const value = strictObject(input, ["symbol", "limit"]);
1387
+ return { symbol: requiredString(value, "symbol"), limit: optionalInteger(value, "limit", 20, 1, 100) };
1388
+ },
1389
+ handler: (input, context) => {
1390
+ const value = input;
1391
+ return context.api.depth(value.symbol, value.limit);
1392
+ }
1393
+ },
1394
+ {
1395
+ name: "market_get_depth_summary",
1396
+ title: "Get Spot Order Book Summary",
1397
+ 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.",
1398
+ cliPath: ["market", "depth-summary"],
1399
+ module: "spot-common",
1400
+ access: "public",
1401
+ operation: "read",
1402
+ riskLevel: "low",
1403
+ inputSchema: { type: "object", properties: { symbol: { type: "string" }, limit: { type: "integer", minimum: 1, maximum: 20 } }, required: ["symbol"], additionalProperties: false },
1404
+ errorCodes: readErrors,
1405
+ validate: (input) => {
1406
+ const value = strictObject(input, ["symbol", "limit"]);
1407
+ return { symbol: requiredString(value, "symbol"), limit: optionalInteger(value, "limit", 10, 1, 20) };
1408
+ },
1409
+ handler: async (input, context) => {
1410
+ const value = input;
1411
+ return summarizeDepth(await context.api.depth(value.symbol, value.limit), value.symbol);
1412
+ }
1413
+ },
1414
+ {
1415
+ name: "market_get_trades",
1416
+ title: "Get Recent Spot Trades",
1417
+ description: "Get recent spot trades for one symbol.",
1418
+ cliPath: ["market", "trades"],
1419
+ module: "spot-common",
1420
+ access: "public",
1421
+ operation: "read",
1422
+ riskLevel: "low",
1423
+ inputSchema: { type: "object", properties: { symbol: { type: "string" }, limit: { type: "integer", minimum: 1, maximum: 100 } }, required: ["symbol"], additionalProperties: false },
1424
+ errorCodes: readErrors,
1425
+ validate: (input) => {
1426
+ const value = strictObject(input, ["symbol", "limit"]);
1427
+ return { symbol: requiredString(value, "symbol"), limit: optionalInteger(value, "limit", 20, 1, 100) };
1428
+ },
1429
+ handler: (input, context) => {
1430
+ const value = input;
1431
+ return context.api.trades(value.symbol, value.limit);
1432
+ }
1433
+ },
1434
+ {
1435
+ name: "market_get_trades_summary",
1436
+ title: "Get Recent Spot Trades Summary",
1437
+ 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.",
1438
+ cliPath: ["market", "trades-summary"],
1439
+ module: "spot-common",
1440
+ access: "public",
1441
+ operation: "read",
1442
+ riskLevel: "low",
1443
+ inputSchema: { type: "object", properties: { symbol: { type: "string" }, limit: { type: "integer", minimum: 1, maximum: 50 } }, required: ["symbol"], additionalProperties: false },
1444
+ errorCodes: readErrors,
1445
+ validate: (input) => {
1446
+ const value = strictObject(input, ["symbol", "limit"]);
1447
+ return { symbol: requiredString(value, "symbol"), limit: optionalInteger(value, "limit", 20, 1, 50) };
1448
+ },
1449
+ handler: async (input, context) => {
1450
+ const value = input;
1451
+ return summarizeTrades(await context.api.trades(value.symbol, value.limit), value.symbol);
1452
+ }
1453
+ },
1454
+ {
1455
+ name: "market_get_klines",
1456
+ title: "Get Spot Klines",
1457
+ description: "Get raw spot candlestick data for one symbol and a supported interval. Use market_get_klines_summary for Agent analysis.",
1458
+ cliPath: ["market", "klines"],
1459
+ module: "spot-common",
1460
+ access: "public",
1461
+ operation: "read",
1462
+ riskLevel: "low",
1463
+ inputSchema: { type: "object", properties: { symbol: { type: "string", description: "Spot symbol, for example ETHUSDT or ETH/USDT." }, interval: { type: "string", enum: KLINE_INTERVALS, description: klineIntervalDescription }, startTime: { type: "integer", description: "Optional inclusive Unix timestamp in milliseconds." }, endTime: { type: "integer", description: "Optional inclusive Unix timestamp in milliseconds." }, timezone: { type: "string", description: klineTimezoneDescription }, limit: { type: "integer", minimum: 1, maximum: 300, description: "Number of newest-first candles. The upstream maximum is 300." } }, required: ["symbol", "interval"], additionalProperties: false },
1464
+ errorCodes: readErrors,
1465
+ validate: (input) => {
1466
+ return validateKlineInput(input, { summary: false });
1467
+ },
1468
+ handler: (input, context) => context.api.klines(input)
1469
+ },
1470
+ {
1471
+ name: "market_get_klines_summary",
1472
+ title: "Get Spot Klines Summary",
1473
+ description: "Get a bounded candle analysis with period change, high/low, latest candle, and a caller-limited candle sample. Defaults to 60min. Prefer this to market_get_klines for Agent analysis.",
1474
+ cliPath: ["market", "klines-summary"],
1475
+ module: "spot-common",
1476
+ access: "public",
1477
+ operation: "read",
1478
+ riskLevel: "low",
1479
+ inputSchema: { type: "object", properties: { symbol: { type: "string", description: "Spot symbol, for example ETHUSDT or ETH/USDT." }, interval: { type: "string", enum: KLINE_INTERVALS, description: `${klineIntervalDescription} Defaults to 60min when omitted.` }, startTime: { type: "integer", description: "Optional inclusive Unix timestamp in milliseconds." }, endTime: { type: "integer", description: "Optional inclusive Unix timestamp in milliseconds." }, timezone: { type: "string", description: klineTimezoneDescription }, limit: { type: "integer", minimum: 1, maximum: 100, description: "Number of candles included in the bounded analysis." } }, required: ["symbol"], additionalProperties: false },
1480
+ errorCodes: readErrors,
1481
+ validate: (input) => {
1482
+ return validateKlineInput(input, { summary: true });
1483
+ },
1484
+ handler: async (input, context) => {
1485
+ const value = input;
1486
+ return summarizeKlines(await context.api.klines(value), value.symbol, value.interval);
1487
+ }
1488
+ }
1489
+ ];
1490
+
1111
1491
  // ../core/src/tools/margin-tools.ts
1112
1492
  function validateMarginSemanticOrder(input) {
1113
1493
  const value = strictObject(input, ["symbol", "side", "type", "quoteAmount", "baseQuantity", "price", "newClientOrderId", "isolated"]);
@@ -2126,16 +2506,36 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2126
2506
  import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
2127
2507
  var CAPABILITIES_TOOL = "system_get_capabilities";
2128
2508
  var CONFIRM_ACTION_TOOL = "confirm_action";
2509
+ var READ_RESULT_OUTPUT_SCHEMA = {
2510
+ type: "object",
2511
+ properties: {
2512
+ ok: { type: "boolean", const: true },
2513
+ data: {
2514
+ type: "object",
2515
+ properties: {
2516
+ dataType: { type: "string", enum: ["array", "object", "scalar", "null"] },
2517
+ items: { type: "array" },
2518
+ count: { type: "integer", minimum: 0 },
2519
+ value: {}
2520
+ },
2521
+ required: ["dataType"]
2522
+ }
2523
+ },
2524
+ required: ["ok", "data"]
2525
+ };
2129
2526
  function result(data) {
2130
2527
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
2131
2528
  }
2132
- function formatMcpData(toolName, data) {
2529
+ function toMcpReadResult(data) {
2530
+ const payload = { ok: true, data };
2531
+ return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }], structuredContent: payload };
2532
+ }
2533
+ function formatMcpData(data) {
2133
2534
  if (Array.isArray(data)) {
2134
2535
  return {
2135
2536
  dataType: "array",
2136
2537
  items: data,
2137
- count: data.length,
2138
- ...toolName === "market_get_ticker" ? { tickers: data } : {}
2538
+ count: data.length
2139
2539
  };
2140
2540
  }
2141
2541
  if (data === null || data === void 0) return { dataType: "null", value: null };
@@ -2150,8 +2550,9 @@ function toMcpTool(tool) {
2150
2550
  return {
2151
2551
  name: tool.name,
2152
2552
  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." : ""}`,
2553
+ 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
2554
  inputSchema: tool.inputSchema,
2555
+ ...tool.operation === "read" ? { outputSchema: READ_RESULT_OUTPUT_SCHEMA } : {},
2155
2556
  annotations: {
2156
2557
  readOnlyHint: tool.operation === "read",
2157
2558
  destructiveHint: tool.riskLevel === "high",
@@ -2160,6 +2561,9 @@ function toMcpTool(tool) {
2160
2561
  }
2161
2562
  };
2162
2563
  }
2564
+ function isMcpVisible(tool) {
2565
+ return tool.mcpVisible !== false;
2566
+ }
2163
2567
  function prepareToolName(tool) {
2164
2568
  const [prefix, ...rest] = tool.name.split("_");
2165
2569
  return `${prefix ?? "spot"}_prepare_${rest.join("_")}`;
@@ -2177,10 +2581,10 @@ function createServer(profileName, readOnly) {
2177
2581
  const registry = createToolRegistry();
2178
2582
  const writeExecutor = new ToolWriteExecutor(registry);
2179
2583
  const server = new Server(
2180
- { name: "ai-hub-agent-trade", version: "0.1.5" },
2584
+ { name: "ai-hub-agent-trade", version: "0.1.7" },
2181
2585
  {
2182
2586
  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."
2587
+ instructions: "Every successful read-tool response provides both JSON text and MCP structuredContent in the envelope { ok: true, data: ... }. All read-tool data is normalized: dataType=array means use data.items and data.count; dataType=object or scalar means use data.value; dataType=null means no value. Inspect data.dataType before formatting and never assume undocumented nested keys. For symbol lookup, use market_search_symbols. It returns an object whose bounded matches are at data.value.items; the complete raw symbols payload is intentionally unavailable in MCP. For broad market questions, call market_get_ticker_summary, market_get_depth_summary, market_get_trades_summary, or market_get_klines_summary instead of fetching large raw payloads. Prepare/confirm tools return their documented action payload directly in data. For every state-changing action, call only a spot_prepare_* or margin_prepare_* tool first and show its exact summary to the user. Stop and wait for a new, explicit user confirmation message. Only then call confirm_action with that new message verbatim in userConfirmation. Never call prepare and confirm consecutively for one user instruction; never infer confirmation from prior intent, silence, or an Agent-generated message. For spot and margin orders, MARKET BUY always uses quoteAmount (the quote asset to spend); MARKET SELL uses baseQuantity (the base asset to sell). Never reinterpret a requested base quantity as quoteAmount."
2184
2588
  }
2185
2589
  );
2186
2590
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
@@ -2192,7 +2596,7 @@ function createServer(profileName, readOnly) {
2192
2596
  inputSchema: { type: "object", additionalProperties: false },
2193
2597
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
2194
2598
  },
2195
- ...registry.list({ readOnly }).flatMap((tool) => tool.operation === "write" ? [toPrepareMcpTool(tool)] : [toMcpTool(tool)]),
2599
+ ...registry.list({ readOnly }).filter(isMcpVisible).flatMap((tool) => tool.operation === "write" ? [toPrepareMcpTool(tool)] : [toMcpTool(tool)]),
2196
2600
  ...readOnly ? [] : [{
2197
2601
  name: CONFIRM_ACTION_TOOL,
2198
2602
  title: "Confirm Prepared Action",
@@ -2211,7 +2615,7 @@ function createServer(profileName, readOnly) {
2211
2615
  data: {
2212
2616
  profile: { name: context.profile.name, host: new URL(context.profile.openApiBaseUrl).host, configVersion: context.profile.configVersion },
2213
2617
  readOnly,
2214
- capabilities: registry.capabilities(context, { readOnly })
2618
+ capabilities: registry.capabilities(context, { readOnly }).filter((capability) => isMcpVisible(registry.byName(capability.name)))
2215
2619
  }
2216
2620
  });
2217
2621
  }
@@ -2222,12 +2626,16 @@ function createServer(profileName, readOnly) {
2222
2626
  }
2223
2627
  return result({ ok: true, data: await writeExecutor.confirm(input.confirmationId, input.userConfirmation, context) });
2224
2628
  }
2225
- const preparedTool = registry.list({ readOnly: false }).find((tool) => tool.operation === "write" && prepareToolName(tool) === request.params.name);
2629
+ const preparedTool = registry.list({ readOnly: false }).find((tool2) => tool2.operation === "write" && prepareToolName(tool2) === request.params.name);
2226
2630
  if (preparedTool) {
2227
2631
  return result({ ok: true, data: await writeExecutor.prepare(preparedTool.name, request.params.arguments ?? {}, context) });
2228
2632
  }
2633
+ const tool = registry.byName(request.params.name, { readOnly });
2634
+ if (!isMcpVisible(tool)) {
2635
+ throw new AiHubError("AI_HUB_TOOL_NOT_AVAILABLE", `Tool "${request.params.name}" is not available through MCP. Use market_search_symbols for symbol lookup.`);
2636
+ }
2229
2637
  const data = await registry.execute(request.params.name, request.params.arguments ?? {}, context, { readOnly });
2230
- return result({ ok: true, data: formatMcpData(request.params.name, data) });
2638
+ return toMcpReadResult(formatMcpData(data));
2231
2639
  } catch (error) {
2232
2640
  return toMcpErrorResult(error);
2233
2641
  }
package/package.json CHANGED
@@ -1,8 +1,13 @@
1
1
  {
2
2
  "name": "@aihubspot/agent-trade-mcp",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
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",