@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.
- package/README.md +14 -2
- package/dist/index.js +552 -144
- 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. 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
|
|
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,134 +866,248 @@ var assetTools = [
|
|
|
866
866
|
}
|
|
867
867
|
];
|
|
868
868
|
|
|
869
|
-
// ../core/src/tools/market-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
2584
|
+
{ name: "ai-hub-agent-trade", version: "0.1.7" },
|
|
2181
2585
|
{
|
|
2182
2586
|
capabilities: { tools: {} },
|
|
2183
|
-
instructions: "Every successful tool response
|
|
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((
|
|
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
|
|
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.
|
|
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",
|