@aihubspot/agent-trade-mcp 0.1.7 → 0.1.9
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 +5 -2
- package/dist/index.js +291 -61
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -23,7 +23,7 @@ Setup registers the currently installed MCP binary through its absolute Node run
|
|
|
23
23
|
|
|
24
24
|
Use `spot_prepare_market_buy` or `margin_prepare_market_buy` only with `quoteAmount` (for example, the exact USDT amount to spend for `ETHUSDT`). Use `spot_prepare_market_sell` or `margin_prepare_market_sell` only with `baseQuantity` (the exact ETH amount to sell for `ETHUSDT`). A market buy cannot guarantee an exact base-asset quantity; it must never reinterpret a requested base quantity as a quote amount.
|
|
25
25
|
|
|
26
|
-
Before any order preview, MCP lazily loads `/sapi/v2/symbols` once per local profile and caches the symbol rules in memory
|
|
26
|
+
Before any order preview, MCP lazily loads `/sapi/v2/symbols` once per local profile and caches the symbol rules for one hour in memory and an isolated local cache. Known quantity/price precision and limit-order minimum violations are rejected before confirmation.
|
|
27
27
|
|
|
28
28
|
## Read response
|
|
29
29
|
|
|
@@ -39,7 +39,10 @@ MCP clients that support the current protocol also receive the same envelope in
|
|
|
39
39
|
|
|
40
40
|
Use these tools for requests that would otherwise return a broad market payload:
|
|
41
41
|
|
|
42
|
-
- `
|
|
42
|
+
- `market_get_symbol_overview`: counts by quote asset and a small sample. Use this first for a generic request to list trading pairs.
|
|
43
|
+
- `market_list_symbols`: a paged, optionally quote-asset-filtered list without order-rule metadata.
|
|
44
|
+
- `market_search_symbols`: required-keyword lookup with at most 20 basic matching rows. Do not use it for a generic pair list.
|
|
45
|
+
- `market_get_symbol_info`: exact precision and minimum order rules for one symbol only.
|
|
43
46
|
- `market_get_ticker_summary`: watchlist, gainers, losers, and quote-volume leaders.
|
|
44
47
|
- `market_get_depth_summary`: best bid/ask, spread, and up to 20 levels per side.
|
|
45
48
|
- `market_get_trades_summary`: price range, buy/sell statistics, and up to 50 recent trades.
|
package/dist/index.js
CHANGED
|
@@ -656,29 +656,82 @@ function optionalClientOrderId(input) {
|
|
|
656
656
|
}
|
|
657
657
|
|
|
658
658
|
// ../core/src/tools/asset-tools.ts
|
|
659
|
-
var
|
|
659
|
+
var ACCOUNT_TYPES = ["1", "2", "3", "4", "5"];
|
|
660
|
+
var ACCOUNT_TYPE_NAMES = {
|
|
661
|
+
"1": "Spot",
|
|
662
|
+
"2": "Isolated Margin",
|
|
663
|
+
"3": "Cross Margin",
|
|
664
|
+
"4": "C2C",
|
|
665
|
+
"5": "Derivatives"
|
|
666
|
+
};
|
|
667
|
+
var accountTypeSchema = {
|
|
668
|
+
type: "string",
|
|
669
|
+
oneOf: [
|
|
670
|
+
{ const: "1", title: "Spot (1)" },
|
|
671
|
+
{ const: "2", title: "Isolated Margin (2; symbol required)" },
|
|
672
|
+
{ const: "3", title: "Cross Margin (3)" },
|
|
673
|
+
{ const: "4", title: "C2C (4)" },
|
|
674
|
+
{ const: "5", title: "Derivatives (5)" }
|
|
675
|
+
],
|
|
676
|
+
description: "Account type: 1=Spot, 2=Isolated Margin (requires symbol), 3=Cross Margin, 4=C2C, 5=Derivatives. Do not use 2 for Derivatives."
|
|
677
|
+
};
|
|
678
|
+
var spotDerivativeAccounts = ["EXCHANGE", "FUTURE"];
|
|
660
679
|
function accountType(value, name) {
|
|
661
680
|
const result2 = requiredString(value, name);
|
|
662
|
-
if (!
|
|
681
|
+
if (!ACCOUNT_TYPES.includes(result2)) throw new AiHubError("AI_HUB_INVALID_ARGUMENT", `${name} must be an account type: 1=Spot, 2=Isolated Margin, 3=Cross Margin, 4=C2C, or 5=Derivatives.`);
|
|
663
682
|
return result2;
|
|
664
683
|
}
|
|
684
|
+
function accountTypeName(value) {
|
|
685
|
+
return ACCOUNT_TYPE_NAMES[value];
|
|
686
|
+
}
|
|
687
|
+
function isIsolatedMarginTransfer(fromAccountType, toAccountType) {
|
|
688
|
+
return fromAccountType === "2" || toAccountType === "2";
|
|
689
|
+
}
|
|
665
690
|
function requiredTransfer(input) {
|
|
666
691
|
const value = strictObject(input, ["fromAccountType", "toAccountType", "symbol", "coinSymbol", "amount"]);
|
|
667
|
-
|
|
692
|
+
const fromAccountType = accountType(value, "fromAccountType");
|
|
693
|
+
const toAccountType = accountType(value, "toAccountType");
|
|
694
|
+
if (fromAccountType === toAccountType) throw new AiHubError("AI_HUB_INVALID_ARGUMENT", "fromAccountType and toAccountType must be different.");
|
|
695
|
+
const symbol = optionalString(value, "symbol");
|
|
696
|
+
if (isIsolatedMarginTransfer(fromAccountType, toAccountType) && !symbol) {
|
|
697
|
+
throw new AiHubError("AI_HUB_ISOLATED_MARGIN_SYMBOL_REQUIRED", "symbol is required when fromAccountType or toAccountType is 2 (Isolated Margin). Provide the isolated-margin trading pair, for example ETHUSDT.");
|
|
698
|
+
}
|
|
699
|
+
return { fromAccountType, toAccountType, coinSymbol: requiredString(value, "coinSymbol"), amount: positiveDecimal(value, "amount"), ...symbol ? { symbol } : {} };
|
|
668
700
|
}
|
|
669
701
|
function transferQuery(input) {
|
|
670
702
|
const value = strictObject(input, ["fromAccountType", "toAccountType", "symbol", "coinSymbol", "page", "pageSize"]);
|
|
671
|
-
|
|
703
|
+
const fromAccountType = accountType(value, "fromAccountType");
|
|
704
|
+
const toAccountType = accountType(value, "toAccountType");
|
|
705
|
+
const symbol = optionalString(value, "symbol");
|
|
706
|
+
if (isIsolatedMarginTransfer(fromAccountType, toAccountType) && !symbol) {
|
|
707
|
+
throw new AiHubError("AI_HUB_ISOLATED_MARGIN_SYMBOL_REQUIRED", "symbol is required when fromAccountType or toAccountType is 2 (Isolated Margin). Provide the isolated-margin trading pair, for example ETHUSDT.");
|
|
708
|
+
}
|
|
709
|
+
return { fromAccountType, toAccountType, page: optionalPositiveInteger(value, "page", 1), pageSize: optionalPositiveInteger(value, "pageSize", 20, 100), ...symbol ? { symbol } : {}, ...optionalString(value, "coinSymbol") ? { coinSymbol: optionalString(value, "coinSymbol") } : {} };
|
|
672
710
|
}
|
|
673
711
|
function transferAccounts(input) {
|
|
674
712
|
const value = strictObject(input, ["coinSymbol", "amount", "fromAccount", "toAccount"]);
|
|
675
|
-
|
|
713
|
+
const fromAccount = requiredString(value, "fromAccount").toUpperCase();
|
|
714
|
+
const toAccount = requiredString(value, "toAccount").toUpperCase();
|
|
715
|
+
if (!spotDerivativeAccounts.includes(fromAccount) || !spotDerivativeAccounts.includes(toAccount) || fromAccount === toAccount) {
|
|
716
|
+
throw new AiHubError("AI_HUB_INVALID_ARGUMENT", "account transfer supports only EXCHANGE -> FUTURE or FUTURE -> EXCHANGE. Use wallet transfer for isolated margin, cross margin, or C2C.");
|
|
717
|
+
}
|
|
718
|
+
return { coinSymbol: requiredString(value, "coinSymbol"), amount: positiveDecimal(value, "amount"), fromAccount, toAccount };
|
|
676
719
|
}
|
|
677
720
|
function transferAccountHistory(input) {
|
|
678
721
|
const value = strictObject(input, ["transferId", "coinSymbol", "fromAccount", "toAccount", "startTime", "endTime", "page", "limit"]);
|
|
679
722
|
const transferId = optionalString(value, "transferId");
|
|
680
723
|
if (!transferId && (!optionalString(value, "fromAccount") || !optionalString(value, "toAccount"))) throw new AiHubError("AI_HUB_INVALID_ARGUMENT", "fromAccount and toAccount are required when transferId is omitted.");
|
|
681
|
-
|
|
724
|
+
const fromAccount = optionalString(value, "fromAccount")?.toUpperCase();
|
|
725
|
+
const toAccount = optionalString(value, "toAccount")?.toUpperCase();
|
|
726
|
+
if (!transferId && (!spotDerivativeAccounts.includes(fromAccount) || !spotDerivativeAccounts.includes(toAccount) || fromAccount === toAccount)) {
|
|
727
|
+
throw new AiHubError("AI_HUB_INVALID_ARGUMENT", "account transfer history supports only EXCHANGE -> FUTURE or FUTURE -> EXCHANGE when transferId is omitted.");
|
|
728
|
+
}
|
|
729
|
+
return { ...transferId ? { transferId } : {}, ...optionalString(value, "coinSymbol") ? { coinSymbol: optionalString(value, "coinSymbol") } : {}, ...fromAccount ? { fromAccount } : {}, ...toAccount ? { toAccount } : {}, ...optionalString(value, "startTime") ? { startTime: optionalString(value, "startTime") } : {}, ...optionalString(value, "endTime") ? { endTime: optionalString(value, "endTime") } : {}, page: optionalPositiveInteger(value, "page", 1), limit: optionalPositiveInteger(value, "limit", 20, 100) };
|
|
730
|
+
}
|
|
731
|
+
function withAccountTypeMetadata(response, value) {
|
|
732
|
+
const metadata = { accountType: value, accountTypeName: accountTypeName(value) };
|
|
733
|
+
if (response && typeof response === "object" && !Array.isArray(response)) return { ...response, ...metadata };
|
|
734
|
+
return { ...metadata, response };
|
|
682
735
|
}
|
|
683
736
|
function pagedDates(input) {
|
|
684
737
|
const value = strictObject(input, ["startTime", "endTime", "page", "pageSize"]);
|
|
@@ -691,18 +744,21 @@ function coinOnly(input) {
|
|
|
691
744
|
var assetTools = [
|
|
692
745
|
{
|
|
693
746
|
name: "account_transfer",
|
|
694
|
-
title: "Transfer Between
|
|
695
|
-
description: "Transfer
|
|
747
|
+
title: "Transfer Between Spot and Derivatives",
|
|
748
|
+
description: "Transfer only between Spot and Derivatives after confirmation. Use fromAccount=EXCHANGE and toAccount=FUTURE for Spot to Derivatives; use the reverse for Derivatives to Spot. Do not pass numeric account types to this Tool.",
|
|
696
749
|
cliPath: ["account", "transfer"],
|
|
697
750
|
module: "spot-account",
|
|
698
751
|
access: "signed",
|
|
699
752
|
operation: "write",
|
|
700
753
|
riskLevel: "high",
|
|
701
|
-
inputSchema: { type: "object", properties: { coinSymbol: { type: "string" }, amount: { type: "string" }, fromAccount: { type: "string" }, toAccount: { type: "string" } }, required: ["coinSymbol", "amount", "fromAccount", "toAccount"], additionalProperties: false },
|
|
754
|
+
inputSchema: { type: "object", properties: { coinSymbol: { type: "string" }, amount: { type: "string" }, fromAccount: { type: "string", enum: spotDerivativeAccounts, description: "EXCHANGE means Spot; FUTURE means Derivatives." }, toAccount: { type: "string", enum: spotDerivativeAccounts, description: "EXCHANGE means Spot; FUTURE means Derivatives." } }, required: ["coinSymbol", "amount", "fromAccount", "toAccount"], additionalProperties: false },
|
|
702
755
|
errorCodes: writeErrors,
|
|
703
756
|
validate: transferAccounts,
|
|
704
757
|
handler: (input, context) => context.api.signedPost("/sapi/v1/asset/transfer", input, signed(context)),
|
|
705
|
-
writeSummary: (input) =>
|
|
758
|
+
writeSummary: (input) => {
|
|
759
|
+
const value = input;
|
|
760
|
+
return { action: "spot_derivatives_transfer", coinSymbol: value.coinSymbol, amount: value.amount, fromAccount: value.fromAccount, fromAccountName: value.fromAccount === "EXCHANGE" ? "Spot" : "Derivatives", toAccount: value.toAccount, toAccountName: value.toAccount === "EXCHANGE" ? "Spot" : "Derivatives" };
|
|
761
|
+
}
|
|
706
762
|
},
|
|
707
763
|
{
|
|
708
764
|
name: "account_get_transfer_history",
|
|
@@ -721,28 +777,31 @@ var assetTools = [
|
|
|
721
777
|
{
|
|
722
778
|
name: "wallet_universal_transfer",
|
|
723
779
|
title: "Universal Asset Transfer",
|
|
724
|
-
description: "Transfer between account types after confirmation.",
|
|
780
|
+
description: "Transfer between numeric account types after confirmation: 1=Spot, 2=Isolated Margin, 3=Cross Margin, 4=C2C, 5=Derivatives. Account type 2 is not Derivatives and requires symbol. For a straightforward Spot to Derivatives transfer, prefer account_transfer with EXCHANGE -> FUTURE.",
|
|
725
781
|
cliPath: ["wallet", "transfer"],
|
|
726
782
|
module: "spot-deposit-withdraw",
|
|
727
783
|
access: "signed",
|
|
728
784
|
operation: "write",
|
|
729
785
|
riskLevel: "high",
|
|
730
|
-
inputSchema: { type: "object", properties: { fromAccountType:
|
|
786
|
+
inputSchema: { type: "object", properties: { fromAccountType: accountTypeSchema, toAccountType: accountTypeSchema, symbol: { type: "string", description: "Required isolated-margin trading pair when either account type is 2, for example ETHUSDT. Not a Derivatives identifier." }, coinSymbol: { type: "string" }, amount: { type: "string" } }, required: ["fromAccountType", "toAccountType", "coinSymbol", "amount"], additionalProperties: false },
|
|
731
787
|
errorCodes: writeErrors,
|
|
732
788
|
validate: requiredTransfer,
|
|
733
789
|
handler: (input, context) => context.api.signedPost("/sapi/v1/asset/universal_transfer", input, signed(context)),
|
|
734
|
-
writeSummary: (input) =>
|
|
790
|
+
writeSummary: (input) => {
|
|
791
|
+
const value = input;
|
|
792
|
+
return { action: "universal_transfer", fromAccountType: value.fromAccountType, fromAccountTypeName: accountTypeName(String(value.fromAccountType)), toAccountType: value.toAccountType, toAccountTypeName: accountTypeName(String(value.toAccountType)), coinSymbol: value.coinSymbol, amount: value.amount, symbol: value.symbol ?? null };
|
|
793
|
+
}
|
|
735
794
|
},
|
|
736
795
|
{
|
|
737
796
|
name: "wallet_get_universal_transfer_history",
|
|
738
797
|
title: "Get Universal Transfer History",
|
|
739
|
-
description: "Query universal
|
|
798
|
+
description: "Query universal transfer history using account types 1=Spot, 2=Isolated Margin, 3=Cross Margin, 4=C2C, and 5=Derivatives.",
|
|
740
799
|
cliPath: ["wallet", "transfer-history"],
|
|
741
800
|
module: "spot-deposit-withdraw",
|
|
742
801
|
access: "signed",
|
|
743
802
|
operation: "read",
|
|
744
803
|
riskLevel: "low",
|
|
745
|
-
inputSchema: { type: "object", properties: { fromAccountType:
|
|
804
|
+
inputSchema: { type: "object", properties: { fromAccountType: accountTypeSchema, toAccountType: accountTypeSchema, symbol: { type: "string", description: "Isolated-margin trading pair when account type 2 is involved." }, coinSymbol: { type: "string" }, page: { type: "integer" }, pageSize: { type: "integer" } }, required: ["fromAccountType", "toAccountType"], additionalProperties: false },
|
|
746
805
|
errorCodes: signedReadErrors,
|
|
747
806
|
validate: transferQuery,
|
|
748
807
|
handler: (input, context) => context.api.signedPost("/sapi/v1/asset/universal_transfer_query", input, signed(context))
|
|
@@ -795,19 +854,22 @@ var assetTools = [
|
|
|
795
854
|
{
|
|
796
855
|
name: "wallet_get_transferable_assets",
|
|
797
856
|
title: "Get Transferable Assets",
|
|
798
|
-
description: "Query assets
|
|
857
|
+
description: "Query transferable assets for one account type. The response echoes the selected account type and its name: 1=Spot, 2=Isolated Margin, 3=Cross Margin, 4=C2C, 5=Derivatives.",
|
|
799
858
|
cliPath: ["wallet", "transferable-assets"],
|
|
800
859
|
module: "spot-deposit-withdraw",
|
|
801
860
|
access: "signed",
|
|
802
861
|
operation: "read",
|
|
803
862
|
riskLevel: "low",
|
|
804
|
-
inputSchema: { type: "object", properties: { accountType:
|
|
863
|
+
inputSchema: { type: "object", properties: { accountType: accountTypeSchema }, required: ["accountType"], additionalProperties: false },
|
|
805
864
|
errorCodes: signedReadErrors,
|
|
806
865
|
validate: (input) => {
|
|
807
866
|
const value = strictObject(input, ["accountType"]);
|
|
808
867
|
return { accountType: accountType(value, "accountType") };
|
|
809
868
|
},
|
|
810
|
-
handler: (input, context) =>
|
|
869
|
+
handler: async (input, context) => {
|
|
870
|
+
const value = input;
|
|
871
|
+
return withAccountTypeMetadata(await context.api.signedPost("/sapi/v1/asset/account/by_type", value, signed(context)), value.accountType);
|
|
872
|
+
}
|
|
811
873
|
},
|
|
812
874
|
{
|
|
813
875
|
name: "wallet_get_exchange_account",
|
|
@@ -970,47 +1032,112 @@ function summarizeTickers(value, options) {
|
|
|
970
1032
|
topByQuoteVolume: [...rows2].sort(compareByNumericField("amount", "desc")).slice(0, limit).map(tickerItem)
|
|
971
1033
|
};
|
|
972
1034
|
}
|
|
973
|
-
function
|
|
1035
|
+
function normalizedSymbol(value) {
|
|
1036
|
+
return value.replaceAll("/", "").trim().toUpperCase();
|
|
1037
|
+
}
|
|
1038
|
+
function parsedSymbolRows(value) {
|
|
974
1039
|
const payload = record(value, "Symbols response must be an object.");
|
|
975
1040
|
const rows2 = records(payload.symbols, "Symbols response must contain a symbols array.");
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
1041
|
+
return rows2.map((item) => {
|
|
1042
|
+
const apiSymbol = text(item.symbol);
|
|
1043
|
+
const symbol = text(item.SymbolName) ?? apiSymbol ?? "";
|
|
1044
|
+
return {
|
|
1045
|
+
symbol,
|
|
1046
|
+
apiSymbol,
|
|
1047
|
+
baseAsset: text(item.baseAssetName) ?? text(item.baseAsset),
|
|
1048
|
+
quoteAsset: text(item.quoteAssetName) ?? text(item.quoteAsset),
|
|
1049
|
+
pricePrecision: item.pricePrecision ?? null,
|
|
1050
|
+
quantityPrecision: item.quantityPrecision ?? null,
|
|
1051
|
+
limitVolumeMin: text(item.limitVolumeMin),
|
|
1052
|
+
limitPriceMin: text(item.limitPriceMin),
|
|
1053
|
+
limitAmountMin: text(item.limitAmountMin)
|
|
1054
|
+
};
|
|
1055
|
+
}).filter((item) => Boolean(item.symbol));
|
|
1056
|
+
}
|
|
1057
|
+
function quoteAssetCounts(rows2) {
|
|
1058
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1059
|
+
for (const item of rows2) {
|
|
1060
|
+
const quote = item.quoteAsset?.toUpperCase();
|
|
1061
|
+
if (quote) counts.set(quote, (counts.get(quote) ?? 0) + 1);
|
|
1062
|
+
}
|
|
1063
|
+
return [...counts.entries()].sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0])).map(([asset, count]) => ({ asset, count }));
|
|
1064
|
+
}
|
|
1065
|
+
function basicSymbolItem(item) {
|
|
1066
|
+
return {
|
|
1067
|
+
symbol: item.symbol,
|
|
1068
|
+
apiSymbol: item.apiSymbol,
|
|
1069
|
+
baseAsset: item.baseAsset,
|
|
1070
|
+
quoteAsset: item.quoteAsset
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
1073
|
+
function fullSymbolItem(item) {
|
|
1074
|
+
return {
|
|
1075
|
+
...basicSymbolItem(item),
|
|
1076
|
+
pricePrecision: item.pricePrecision,
|
|
1077
|
+
quantityPrecision: item.quantityPrecision,
|
|
1078
|
+
limitVolumeMin: item.limitVolumeMin,
|
|
1079
|
+
limitPriceMin: item.limitPriceMin,
|
|
1080
|
+
limitAmountMin: item.limitAmountMin
|
|
1081
|
+
};
|
|
1082
|
+
}
|
|
1083
|
+
function sortSymbolMatches(rows2, query) {
|
|
1084
|
+
const normalizedQuery = query?.trim().toUpperCase();
|
|
1085
|
+
return [...rows2].sort((left, right) => {
|
|
1086
|
+
if (!normalizedQuery) return left.symbol.localeCompare(right.symbol);
|
|
985
1087
|
const rank = (item) => {
|
|
986
|
-
const symbol =
|
|
987
|
-
return symbol.startsWith(`${
|
|
1088
|
+
const symbol = item.symbol.toUpperCase();
|
|
1089
|
+
return symbol.startsWith(`${normalizedQuery}/`) ? 0 : symbol.startsWith(normalizedQuery) ? 1 : 2;
|
|
988
1090
|
};
|
|
989
|
-
return rank(left) - rank(right);
|
|
1091
|
+
return rank(left) - rank(right) || left.symbol.localeCompare(right.symbol);
|
|
990
1092
|
});
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
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
|
-
}));
|
|
1093
|
+
}
|
|
1094
|
+
function summarizeSymbolOverview(value, options) {
|
|
1095
|
+
const rows2 = parsedSymbolRows(value);
|
|
1007
1096
|
return {
|
|
1008
1097
|
totalSymbols: rows2.length,
|
|
1098
|
+
quoteAssetCounts: quoteAssetCounts(rows2),
|
|
1099
|
+
sampleSymbols: sortSymbolMatches(rows2).slice(0, options.limit).map((item) => item.symbol)
|
|
1100
|
+
};
|
|
1101
|
+
}
|
|
1102
|
+
function listSymbols(value, options) {
|
|
1103
|
+
const allRows = parsedSymbolRows(value);
|
|
1104
|
+
const quoteAsset = options.quoteAsset?.trim().toUpperCase();
|
|
1105
|
+
const matches = sortSymbolMatches(allRows.filter((item) => !quoteAsset || item.quoteAsset?.toUpperCase() === quoteAsset));
|
|
1106
|
+
const items = matches.slice(options.offset, options.offset + options.limit).map(basicSymbolItem);
|
|
1107
|
+
const nextOffset = options.offset + items.length;
|
|
1108
|
+
return {
|
|
1109
|
+
totalSymbols: allRows.length,
|
|
1009
1110
|
matchedSymbols: matches.length,
|
|
1010
|
-
|
|
1111
|
+
quoteAsset: quoteAsset ?? null,
|
|
1112
|
+
offset: options.offset,
|
|
1113
|
+
limit: options.limit,
|
|
1114
|
+
nextOffset: nextOffset < matches.length ? nextOffset : null,
|
|
1011
1115
|
items
|
|
1012
1116
|
};
|
|
1013
1117
|
}
|
|
1118
|
+
function searchSymbols(value, options) {
|
|
1119
|
+
const allRows = parsedSymbolRows(value);
|
|
1120
|
+
const query = options.query.trim().toUpperCase();
|
|
1121
|
+
const quoteAsset = options.quoteAsset?.trim().toUpperCase();
|
|
1122
|
+
const matches = allRows.filter((item) => {
|
|
1123
|
+
const symbol = item.symbol.toUpperCase();
|
|
1124
|
+
const apiSymbol = item.apiSymbol?.toUpperCase() ?? "";
|
|
1125
|
+
return (symbol.includes(query) || apiSymbol.includes(query)) && (!quoteAsset || item.quoteAsset?.toUpperCase() === quoteAsset);
|
|
1126
|
+
});
|
|
1127
|
+
const sortedMatches = sortSymbolMatches(matches, query);
|
|
1128
|
+
return {
|
|
1129
|
+
matchedSymbols: matches.length,
|
|
1130
|
+
query,
|
|
1131
|
+
quoteAsset: quoteAsset ?? null,
|
|
1132
|
+
items: sortedMatches.slice(0, options.limit).map(basicSymbolItem)
|
|
1133
|
+
};
|
|
1134
|
+
}
|
|
1135
|
+
function getSymbolInfo(value, symbol) {
|
|
1136
|
+
const target = normalizedSymbol(symbol);
|
|
1137
|
+
const item = parsedSymbolRows(value).find((row) => normalizedSymbol(row.symbol) === target || row.apiSymbol !== null && normalizedSymbol(row.apiSymbol) === target);
|
|
1138
|
+
if (!item) throw new AiHubError("AI_HUB_SYMBOL_NOT_FOUND", `Symbol "${symbol}" was not returned by the configured tenant OpenAPI symbols endpoint.`);
|
|
1139
|
+
return fullSymbolItem(item);
|
|
1140
|
+
}
|
|
1014
1141
|
function level(value) {
|
|
1015
1142
|
if (!Array.isArray(value)) return { price: null, quantity: null };
|
|
1016
1143
|
return { price: text(value[0]), quantity: text(value[1]) };
|
|
@@ -1107,13 +1234,53 @@ function summarizeKlines(value, symbol, interval) {
|
|
|
1107
1234
|
}
|
|
1108
1235
|
|
|
1109
1236
|
// ../core/src/tools/symbol-rules.ts
|
|
1237
|
+
import { createHash as createHash4 } from "crypto";
|
|
1238
|
+
import { chmod as chmod2, mkdir as mkdir2, readFile as readFile2, rename as rename2, rm, writeFile as writeFile2 } from "fs/promises";
|
|
1239
|
+
import { homedir as homedir2 } from "os";
|
|
1240
|
+
import { join as join2 } from "path";
|
|
1110
1241
|
var CACHE_TTL_MS = 60 * 60 * 1e3;
|
|
1111
1242
|
var cache = /* @__PURE__ */ new Map();
|
|
1112
1243
|
var pendingLoads = /* @__PURE__ */ new Map();
|
|
1113
1244
|
function cacheKey(context) {
|
|
1114
1245
|
return `${context.profile.name}\0${context.profile.configVersion}\0${context.profile.openApiBaseUrl}`;
|
|
1115
1246
|
}
|
|
1116
|
-
function
|
|
1247
|
+
function persistentCacheEnabled() {
|
|
1248
|
+
return process.env.AI_HUB_DISABLE_PERSISTENT_CACHE !== "1";
|
|
1249
|
+
}
|
|
1250
|
+
function persistentCacheDirectory() {
|
|
1251
|
+
return process.env.AI_HUB_CACHE_DIR ?? join2(homedir2(), ".ai-hub", "cache");
|
|
1252
|
+
}
|
|
1253
|
+
function persistentCachePath(key) {
|
|
1254
|
+
const hash2 = createHash4("sha256").update(key).digest("hex");
|
|
1255
|
+
return join2(persistentCacheDirectory(), `symbols-${hash2}.json`);
|
|
1256
|
+
}
|
|
1257
|
+
async function loadPersistentSnapshot(key) {
|
|
1258
|
+
if (!persistentCacheEnabled()) return void 0;
|
|
1259
|
+
try {
|
|
1260
|
+
const parsed = JSON.parse(await readFile2(persistentCachePath(key), "utf8"));
|
|
1261
|
+
if (typeof parsed.expiresAt !== "number" || !Number.isFinite(parsed.expiresAt) || parsed.expiresAt <= Date.now() || !("response" in parsed)) return void 0;
|
|
1262
|
+
return { expiresAt: parsed.expiresAt, response: parsed.response, rules: parseRules(parsed.response) };
|
|
1263
|
+
} catch {
|
|
1264
|
+
return void 0;
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
async function persistSnapshot(key, entry) {
|
|
1268
|
+
if (!persistentCacheEnabled()) return;
|
|
1269
|
+
const directory = persistentCacheDirectory();
|
|
1270
|
+
const filePath = persistentCachePath(key);
|
|
1271
|
+
const temporaryPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
1272
|
+
try {
|
|
1273
|
+
await mkdir2(directory, { recursive: true, mode: 448 });
|
|
1274
|
+
await chmod2(directory, 448);
|
|
1275
|
+
await writeFile2(temporaryPath, JSON.stringify({ expiresAt: entry.expiresAt, response: entry.response }), { mode: 384 });
|
|
1276
|
+
await chmod2(temporaryPath, 384);
|
|
1277
|
+
await rename2(temporaryPath, filePath);
|
|
1278
|
+
await chmod2(filePath, 384);
|
|
1279
|
+
} catch {
|
|
1280
|
+
await rm(temporaryPath, { force: true }).catch(() => void 0);
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
function normalizedSymbol2(symbol) {
|
|
1117
1284
|
return symbol.replaceAll("/", "").trim().toUpperCase();
|
|
1118
1285
|
}
|
|
1119
1286
|
function stringValue(value) {
|
|
@@ -1140,7 +1307,7 @@ function parseRules(response) {
|
|
|
1140
1307
|
for (const row of rows(response)) {
|
|
1141
1308
|
const symbol = stringValue(row.symbol);
|
|
1142
1309
|
if (!symbol) continue;
|
|
1143
|
-
result2.set(
|
|
1310
|
+
result2.set(normalizedSymbol2(symbol), {
|
|
1144
1311
|
symbol,
|
|
1145
1312
|
quantityPrecision: nonNegativeInteger(row.quantityPrecision),
|
|
1146
1313
|
pricePrecision: nonNegativeInteger(row.pricePrecision),
|
|
@@ -1157,11 +1324,18 @@ async function loadSnapshot(context) {
|
|
|
1157
1324
|
if (current && current.expiresAt > Date.now()) return current;
|
|
1158
1325
|
const pending = pendingLoads.get(key);
|
|
1159
1326
|
if (pending) return pending;
|
|
1160
|
-
const load =
|
|
1327
|
+
const load = (async () => {
|
|
1328
|
+
const persisted = await loadPersistentSnapshot(key);
|
|
1329
|
+
if (persisted) {
|
|
1330
|
+
cache.set(key, persisted);
|
|
1331
|
+
return persisted;
|
|
1332
|
+
}
|
|
1333
|
+
const response = await context.api.symbols();
|
|
1161
1334
|
const entry = { response, rules: parseRules(response), expiresAt: Date.now() + CACHE_TTL_MS };
|
|
1162
1335
|
cache.set(key, entry);
|
|
1336
|
+
await persistSnapshot(key, entry);
|
|
1163
1337
|
return entry;
|
|
1164
|
-
}).finally(() => pendingLoads.delete(key));
|
|
1338
|
+
})().finally(() => pendingLoads.delete(key));
|
|
1165
1339
|
pendingLoads.set(key, load);
|
|
1166
1340
|
return load;
|
|
1167
1341
|
}
|
|
@@ -1170,7 +1344,7 @@ async function getCachedSymbols(context) {
|
|
|
1170
1344
|
}
|
|
1171
1345
|
async function getSymbolRule(context, symbol) {
|
|
1172
1346
|
const entry = await loadSnapshot(context);
|
|
1173
|
-
const rule = entry.rules.get(
|
|
1347
|
+
const rule = entry.rules.get(normalizedSymbol2(symbol));
|
|
1174
1348
|
if (!rule) throw new AiHubError("AI_HUB_SYMBOL_NOT_FOUND", `Symbol "${symbol}" was not returned by the configured tenant OpenAPI symbols endpoint.`);
|
|
1175
1349
|
return rule;
|
|
1176
1350
|
}
|
|
@@ -1227,6 +1401,7 @@ async function preflightSymbolOrder(context, order) {
|
|
|
1227
1401
|
|
|
1228
1402
|
// ../core/src/tools/market-tools.ts
|
|
1229
1403
|
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"];
|
|
1404
|
+
var symbolReadErrors = [...readErrors, "AI_HUB_SYMBOL_NOT_FOUND"];
|
|
1230
1405
|
var KLINE_INTERVALS = ["1min", "5min", "15min", "30min", "60min", "1day", "1week", "1month"];
|
|
1231
1406
|
var KLINE_INTERVAL_ALIASES = {
|
|
1232
1407
|
"1m": "1min",
|
|
@@ -1299,7 +1474,7 @@ var marketTools = [
|
|
|
1299
1474
|
{
|
|
1300
1475
|
name: "market_get_symbols",
|
|
1301
1476
|
title: "Get Spot Symbols",
|
|
1302
|
-
description: "Get complete spot symbol metadata from the configured tenant OpenAPI. Use market_search_symbols for
|
|
1477
|
+
description: "Get complete spot symbol metadata from the configured tenant OpenAPI. Use market_get_symbol_overview, market_list_symbols, market_search_symbols, or market_get_symbol_info for Agent-facing symbol requests, because this response may be large.",
|
|
1303
1478
|
cliPath: ["market", "symbols"],
|
|
1304
1479
|
module: "spot-common",
|
|
1305
1480
|
access: "public",
|
|
@@ -1311,26 +1486,81 @@ var marketTools = [
|
|
|
1311
1486
|
validate: (input) => strictObject(input, []),
|
|
1312
1487
|
handler: (_input, context) => getCachedSymbols(context)
|
|
1313
1488
|
},
|
|
1489
|
+
{
|
|
1490
|
+
name: "market_get_symbol_overview",
|
|
1491
|
+
title: "Get Spot Symbol Overview",
|
|
1492
|
+
description: "Get a small overview of all configured spot symbols: total count, quote-asset counts, and a bounded sample of display symbols. Use this for generic requests such as listing available trading pairs.",
|
|
1493
|
+
cliPath: ["market", "symbols-overview"],
|
|
1494
|
+
module: "spot-common",
|
|
1495
|
+
access: "public",
|
|
1496
|
+
operation: "read",
|
|
1497
|
+
riskLevel: "low",
|
|
1498
|
+
inputSchema: { type: "object", properties: { limit: { type: "integer", minimum: 1, maximum: 20, description: "Number of sample display symbols. Defaults to 12." } }, additionalProperties: false },
|
|
1499
|
+
errorCodes: readErrors,
|
|
1500
|
+
validate: (input) => {
|
|
1501
|
+
const value = strictObject(input, ["limit"]);
|
|
1502
|
+
return { limit: optionalInteger(value, "limit", 12, 1, 20) };
|
|
1503
|
+
},
|
|
1504
|
+
handler: async (input, context) => summarizeSymbolOverview(await getCachedSymbols(context), input)
|
|
1505
|
+
},
|
|
1506
|
+
{
|
|
1507
|
+
name: "market_list_symbols",
|
|
1508
|
+
title: "List Spot Symbols",
|
|
1509
|
+
description: "List one bounded page of configured spot symbols, optionally restricted to a quote asset. This response excludes precision and trading-rule metadata. Use market_get_symbol_info for one exact symbol's rules.",
|
|
1510
|
+
cliPath: ["market", "symbols-list"],
|
|
1511
|
+
module: "spot-common",
|
|
1512
|
+
access: "public",
|
|
1513
|
+
operation: "read",
|
|
1514
|
+
riskLevel: "low",
|
|
1515
|
+
inputSchema: { type: "object", properties: { quoteAsset: { type: "string" }, offset: { type: "integer", minimum: 0, description: "Zero-based page offset. Defaults to 0." }, limit: { type: "integer", minimum: 1, maximum: 50, description: "Page size. Defaults to 20." } }, additionalProperties: false },
|
|
1516
|
+
errorCodes: readErrors,
|
|
1517
|
+
validate: (input) => {
|
|
1518
|
+
const value = strictObject(input, ["quoteAsset", "offset", "limit"]);
|
|
1519
|
+
return {
|
|
1520
|
+
quoteAsset: optionalString(value, "quoteAsset"),
|
|
1521
|
+
offset: optionalInteger(value, "offset", 0, 0, Number.MAX_SAFE_INTEGER),
|
|
1522
|
+
limit: optionalInteger(value, "limit", 20, 1, 50)
|
|
1523
|
+
};
|
|
1524
|
+
},
|
|
1525
|
+
handler: async (input, context) => listSymbols(await getCachedSymbols(context), input)
|
|
1526
|
+
},
|
|
1314
1527
|
{
|
|
1315
1528
|
name: "market_search_symbols",
|
|
1316
1529
|
title: "Search Spot Symbols",
|
|
1317
|
-
description: "Search
|
|
1530
|
+
description: "Search configured spot symbols by a required keyword and return a small matching set without trading-rule metadata. Use market_list_symbols for browsing, or market_get_symbol_info for exact precision and minimum rules.",
|
|
1318
1531
|
cliPath: ["market", "symbols-search"],
|
|
1319
1532
|
module: "spot-common",
|
|
1320
1533
|
access: "public",
|
|
1321
1534
|
operation: "read",
|
|
1322
1535
|
riskLevel: "low",
|
|
1323
|
-
inputSchema: { type: "object", properties: { query: { type: "string" }, quoteAsset: { type: "string" }, limit: { type: "integer", minimum: 1, maximum:
|
|
1536
|
+
inputSchema: { type: "object", properties: { query: { type: "string", minLength: 1, description: "Required asset or symbol keyword, for example BTC." }, quoteAsset: { type: "string" }, limit: { type: "integer", minimum: 1, maximum: 20 } }, required: ["query"], additionalProperties: false },
|
|
1324
1537
|
errorCodes: readErrors,
|
|
1325
1538
|
validate: (input) => {
|
|
1326
1539
|
const value = strictObject(input, ["query", "quoteAsset", "limit"]);
|
|
1327
|
-
return { query:
|
|
1540
|
+
return { query: requiredString(value, "query"), quoteAsset: optionalString(value, "quoteAsset"), limit: optionalInteger(value, "limit", 10, 1, 20) };
|
|
1328
1541
|
},
|
|
1329
1542
|
handler: async (input, context) => {
|
|
1330
1543
|
const value = input;
|
|
1331
|
-
return
|
|
1544
|
+
return searchSymbols(await getCachedSymbols(context), value);
|
|
1332
1545
|
}
|
|
1333
1546
|
},
|
|
1547
|
+
{
|
|
1548
|
+
name: "market_get_symbol_info",
|
|
1549
|
+
title: "Get Spot Symbol Information",
|
|
1550
|
+
description: "Get the exact configured spot symbol's trading-rule metadata, including price and quantity precision plus minimum order constraints. Requires an exact symbol such as BTCUSDT or BTC/USDT.",
|
|
1551
|
+
cliPath: ["market", "symbol-info"],
|
|
1552
|
+
module: "spot-common",
|
|
1553
|
+
access: "public",
|
|
1554
|
+
operation: "read",
|
|
1555
|
+
riskLevel: "low",
|
|
1556
|
+
inputSchema: { type: "object", properties: { symbol: { type: "string", minLength: 1 } }, required: ["symbol"], additionalProperties: false },
|
|
1557
|
+
errorCodes: symbolReadErrors,
|
|
1558
|
+
validate: (input) => {
|
|
1559
|
+
const value = strictObject(input, ["symbol"]);
|
|
1560
|
+
return { symbol: requiredString(value, "symbol") };
|
|
1561
|
+
},
|
|
1562
|
+
handler: async (input, context) => getSymbolInfo(await getCachedSymbols(context), input.symbol)
|
|
1563
|
+
},
|
|
1334
1564
|
{
|
|
1335
1565
|
name: "market_get_ticker",
|
|
1336
1566
|
title: "Get Spot Ticker",
|
|
@@ -2581,10 +2811,10 @@ function createServer(profileName, readOnly) {
|
|
|
2581
2811
|
const registry = createToolRegistry();
|
|
2582
2812
|
const writeExecutor = new ToolWriteExecutor(registry);
|
|
2583
2813
|
const server = new Server(
|
|
2584
|
-
{ name: "ai-hub-agent-trade", version: "0.1.
|
|
2814
|
+
{ name: "ai-hub-agent-trade", version: "0.1.9" },
|
|
2585
2815
|
{
|
|
2586
2816
|
capabilities: { tools: {} },
|
|
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
|
|
2817
|
+
instructions: "Every successful read-tool response provides both JSON text and MCP structuredContent in the envelope { ok: true, data: ... }. All read-tool data is normalized: dataType=array means use data.items and data.count; dataType=object or scalar means use data.value; dataType=null means no value. Inspect data.dataType before formatting and never assume undocumented nested keys. For a generic trading-pair list, call market_get_symbol_overview first; it returns only counts and a small sample. For paged or quote-asset browsing, use market_list_symbols. Use market_search_symbols only when the user gave a keyword; query is required. Use market_get_symbol_info only for exact precision and minimum rules. Do not use a symbol search as a generic all-symbol list. The complete raw symbols payload is intentionally unavailable in MCP. For broad market questions, call market_get_ticker_summary, market_get_depth_summary, market_get_trades_summary, or market_get_klines_summary instead of fetching large raw payloads. Account types are fixed: 1=Spot, 2=Isolated Margin, 3=Cross Margin, 4=C2C, 5=Derivatives. Never call account type 2 Derivatives. For Spot to Derivatives, use account_prepare_transfer with fromAccount=EXCHANGE and toAccount=FUTURE. Use wallet_prepare_universal_transfer for margin or C2C; account type 2 requires its isolated-margin trading pair in symbol. Prepare/confirm tools return their documented action payload directly in data. For every state-changing action, call only a spot_prepare_* or margin_prepare_* tool first and show its exact summary to the user. Stop and wait for a new, explicit user confirmation message. Only then call confirm_action with that new message verbatim in userConfirmation. Never call prepare and confirm consecutively for one user instruction; never infer confirmation from prior intent, silence, or an Agent-generated message. For spot and margin orders, MARKET BUY always uses quoteAmount (the quote asset to spend); MARKET SELL uses baseQuantity (the base asset to sell). Never reinterpret a requested base quantity as quoteAmount."
|
|
2588
2818
|
}
|
|
2589
2819
|
);
|
|
2590
2820
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
@@ -2632,7 +2862,7 @@ function createServer(profileName, readOnly) {
|
|
|
2632
2862
|
}
|
|
2633
2863
|
const tool = registry.byName(request.params.name, { readOnly });
|
|
2634
2864
|
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
|
|
2865
|
+
throw new AiHubError("AI_HUB_TOOL_NOT_AVAILABLE", `Tool "${request.params.name}" is not available through MCP. Use market_get_symbol_overview, market_list_symbols, market_search_symbols, or market_get_symbol_info instead.`);
|
|
2636
2866
|
}
|
|
2637
2867
|
const data = await registry.execute(request.params.name, request.params.arguments ?? {}, context, { readOnly });
|
|
2638
2868
|
return toMcpReadResult(formatMcpData(data));
|