@aihubspot/agent-trade-mcp 0.1.5 → 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.
- package/README.md +14 -2
- package/dist/index.js +387 -21
- 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
|
-
##
|
|
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
|
-
|
|
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
|
|
438
|
-
if (
|
|
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(
|
|
447
|
+
function parseConfig(text2) {
|
|
448
448
|
let parsed;
|
|
449
449
|
try {
|
|
450
|
-
parsed = parse(
|
|
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
|
|
574
|
-
for (const key of Object.keys(
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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:
|
|
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",
|
|
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:
|
|
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",
|
|
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
|
|
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
|
|
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.
|
|
2546
|
+
{ name: "ai-hub-agent-trade", version: "0.1.6" },
|
|
2181
2547
|
{
|
|
2182
2548
|
capabilities: { tools: {} },
|
|
2183
|
-
instructions: "Every successful tool response
|
|
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
|
|
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.
|
|
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",
|