@michaleffffff/mcp-trading-server 3.0.3 → 3.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +48 -408
  3. package/TOOL_EXAMPLES.md +63 -555
  4. package/dist/prompts/tradingGuide.js +16 -19
  5. package/dist/server.js +20 -2
  6. package/dist/services/balanceService.js +4 -4
  7. package/dist/services/marketService.js +48 -12
  8. package/dist/services/tradeService.js +33 -10
  9. package/dist/tools/accountInfo.js +1 -82
  10. package/dist/tools/accountTransfer.js +91 -5
  11. package/dist/tools/adjustMargin.js +1 -1
  12. package/dist/tools/cancelOrders.js +71 -0
  13. package/dist/tools/checkAccountReady.js +69 -0
  14. package/dist/tools/checkApproval.js +1 -1
  15. package/dist/tools/closeAllPositions.js +12 -12
  16. package/dist/tools/closePosition.js +60 -12
  17. package/dist/tools/createPerpMarket.js +1 -1
  18. package/dist/tools/executeTrade.js +6 -6
  19. package/dist/tools/findPool.js +26 -0
  20. package/dist/tools/getAccountSnapshot.js +41 -0
  21. package/dist/tools/getAllTickers.js +5 -4
  22. package/dist/tools/getBaseDetail.js +1 -1
  23. package/dist/tools/getKline.js +7 -4
  24. package/dist/tools/getMyLpHoldings.js +3 -2
  25. package/dist/tools/getNetworkFee.js +1 -1
  26. package/dist/tools/getOrders.js +46 -0
  27. package/dist/tools/getPoolMetadata.js +83 -0
  28. package/dist/tools/getPositionsAll.js +80 -0
  29. package/dist/tools/{getMarketPrice.js → getPrice.js} +8 -5
  30. package/dist/tools/getUserTradingFeeRate.js +1 -1
  31. package/dist/tools/index.js +15 -19
  32. package/dist/tools/listPools.js +53 -0
  33. package/dist/tools/manageLiquidity.js +10 -5
  34. package/dist/tools/manageTpSl.js +163 -0
  35. package/dist/tools/openPositionSimple.js +22 -5
  36. package/dist/tools/searchTools.js +35 -0
  37. package/dist/utils/mappings.js +10 -7
  38. package/dist/utils/slippage.js +23 -0
  39. package/package.json +1 -1
  40. package/dist/tools/cancelAllOrders.js +0 -57
  41. package/dist/tools/cancelOrder.js +0 -22
  42. package/dist/tools/getAccountVipInfo.js +0 -20
  43. package/dist/tools/getKlineLatestBar.js +0 -28
  44. package/dist/tools/getOraclePrice.js +0 -22
  45. package/dist/tools/getPoolList.js +0 -17
  46. package/dist/tools/getPoolSymbolAll.js +0 -16
  47. package/dist/tools/getPositions.js +0 -77
  48. package/dist/tools/marketInfo.js +0 -72
  49. package/dist/tools/orderQueries.js +0 -51
  50. package/dist/tools/poolConfig.js +0 -22
  51. package/dist/tools/positionHistory.js +0 -28
  52. package/dist/tools/searchMarket.js +0 -21
  53. package/dist/tools/setTpSl.js +0 -50
  54. package/dist/tools/updateOrderTpSl.js +0 -34
@@ -0,0 +1,80 @@
1
+ import { z } from "zod";
2
+ import { resolveClient, getChainId } from "../auth/resolveClient.js";
3
+ import { getPositions } from "../services/positionService.js";
4
+ import { getDirectionDesc } from "../utils/mappings.js";
5
+ export const getPositionsAllTool = {
6
+ name: "get_positions_all",
7
+ description: "[ACCOUNT] Get positions (open or history) with optional filters and ROI/PnL metrics.",
8
+ schema: {
9
+ status: z.enum(["OPEN", "HISTORY", "ALL"]).default("OPEN").describe("Filter by status: 'OPEN' (default), 'HISTORY', or 'ALL'"),
10
+ poolId: z.string().optional().describe("Filter by pool ID"),
11
+ limit: z.number().int().positive().optional().describe("Results per page (default 20, for history)"),
12
+ },
13
+ handler: async (args) => {
14
+ try {
15
+ const { client, address } = await resolveClient();
16
+ const chainId = getChainId();
17
+ const results = {};
18
+ if (args.status === "OPEN" || args.status === "ALL") {
19
+ const data = await getPositions(client, address);
20
+ const positions = Array.isArray(data) ? data : (Array.isArray(data?.data) ? data.data : []);
21
+ // Enhance open positions with metrics
22
+ const filtered = args.poolId ? positions.filter((p) => String(p.poolId).toLowerCase() === args.poolId.toLowerCase()) : positions;
23
+ if (filtered.length > 0) {
24
+ const poolIds = [...new Set(filtered.map((p) => p.poolId))];
25
+ const [tickersRes, configs] = await Promise.all([
26
+ client.markets.getTickerList({ chainId, poolIds }),
27
+ Promise.all(poolIds.map(async (pid) => {
28
+ try {
29
+ const { getPoolLevelConfig } = await import("../services/marketService.js");
30
+ const res = await getPoolLevelConfig(client, pid, chainId);
31
+ return { poolId: pid, config: res?.levelConfig || res?.data?.levelConfig || res };
32
+ }
33
+ catch {
34
+ return { poolId: pid, config: null };
35
+ }
36
+ }))
37
+ ]);
38
+ const tickers = Array.isArray(tickersRes) ? tickersRes : (tickersRes?.data ?? []);
39
+ results.open = filtered.map((pos) => {
40
+ const ticker = tickers.find((t) => t.poolId === pos.poolId);
41
+ const currentPrice = Number(ticker?.price || 0);
42
+ const entryPrice = Number(pos.entryPrice || 0);
43
+ const size = Number(pos.size || 0);
44
+ const collateral = Number(pos.collateralAmount || 0);
45
+ const direction = pos.direction;
46
+ const mm = configs.find(c => c.poolId === pos.poolId)?.config?.maintainCollateralRate || 0.02;
47
+ let estimatedPnl = direction === 0 ? (currentPrice - entryPrice) * size : (entryPrice - currentPrice) * size;
48
+ const roi = collateral > 0 ? (estimatedPnl / collateral) * 100 : 0;
49
+ let liqPrice = size > 0 ? (direction === 0 ? (entryPrice * size - collateral) / (size * (1 - mm)) : (entryPrice * size + collateral) / (size * (1 + mm))) : 0;
50
+ if (liqPrice < 0)
51
+ liqPrice = 0;
52
+ return {
53
+ ...pos,
54
+ directionDesc: getDirectionDesc(pos.direction),
55
+ currentPrice: currentPrice.toString(),
56
+ estimatedPnl: estimatedPnl.toFixed(4),
57
+ roi: roi.toFixed(2) + "%",
58
+ liquidationPrice: liqPrice.toFixed(4)
59
+ };
60
+ });
61
+ }
62
+ else {
63
+ results.open = [];
64
+ }
65
+ }
66
+ if (args.status === "HISTORY" || args.status === "ALL") {
67
+ const query = { chainId, poolId: args.poolId, limit: args.limit ?? 20 };
68
+ const historyRes = await client.position.getPositionHistory(query, address);
69
+ results.history = (historyRes?.data || []).map((pos) => ({
70
+ ...pos,
71
+ directionDesc: getDirectionDesc(pos.direction)
72
+ }));
73
+ }
74
+ return { content: [{ type: "text", text: JSON.stringify({ status: "success", data: results }, (_, v) => typeof v === 'bigint' ? v.toString() : v, 2) }] };
75
+ }
76
+ catch (error) {
77
+ return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
78
+ }
79
+ },
80
+ };
@@ -1,18 +1,21 @@
1
1
  import { z } from "zod";
2
2
  import { resolveClient, getChainId } from "../auth/resolveClient.js";
3
- import { getMarketPrice } from "../services/marketService.js";
4
- export const getMarketPriceTool = {
5
- name: "get_market_price",
6
- description: "Get the current market price for a specific pool.",
3
+ import { getMarketPrice, getOraclePrice } from "../services/marketService.js";
4
+ export const getPriceTool = {
5
+ name: "get_price",
6
+ description: "[MARKET] Get the current price for a specific pool. Support both market (impact) and oracle prices.",
7
7
  schema: {
8
8
  poolId: z.string().describe("Pool ID to get price for"),
9
+ priceType: z.enum(["market", "oracle"]).default("market").describe("Type of price to fetch: 'market' (default) or 'oracle'"),
9
10
  chainId: z.number().int().positive().optional().describe("Optional chainId override"),
10
11
  },
11
12
  handler: async (args) => {
12
13
  try {
13
14
  const { client } = await resolveClient();
14
15
  const chainId = args.chainId ?? getChainId();
15
- const data = await getMarketPrice(client, args.poolId, chainId);
16
+ const data = args.priceType === "oracle"
17
+ ? await getOraclePrice(client, args.poolId, chainId)
18
+ : await getMarketPrice(client, args.poolId, chainId);
16
19
  return { content: [{ type: "text", text: JSON.stringify({ status: "success", data }, (k, v) => typeof v === 'bigint' ? v.toString() : v, 2) }] };
17
20
  }
18
21
  catch (error) {
@@ -2,7 +2,7 @@ import { z } from "zod";
2
2
  import { resolveClient, getChainId } from "../auth/resolveClient.js";
3
3
  export const getUserTradingFeeRateTool = {
4
4
  name: "get_user_trading_fee_rate",
5
- description: "Get maker/taker fee rates for a given assetClass and riskTier.",
5
+ description: "[TRADE] Get maker/taker fee rates for a given assetClass and riskTier.",
6
6
  schema: {
7
7
  assetClass: z.coerce.number().int().nonnegative().describe("Asset class ID"),
8
8
  riskTier: z.coerce.number().int().nonnegative().describe("Risk tier"),
@@ -1,36 +1,32 @@
1
1
  // Tools — 交易
2
2
  export { openPositionSimpleTool } from "./openPositionSimple.js";
3
3
  export { executeTradeTool } from "./executeTrade.js";
4
- export { cancelOrderTool } from "./cancelOrder.js";
5
- export { cancelAllOrdersTool } from "./cancelAllOrders.js";
4
+ export { cancelOrdersTool } from "./cancelOrders.js"; // Unified
6
5
  export { closePositionTool } from "./closePosition.js";
7
- export { setTpSlTool } from "./setTpSl.js";
8
- export { updateOrderTpSlTool } from "./updateOrderTpSl.js";
6
+ export { manageTpSlTool } from "./manageTpSl.js"; // Unified
9
7
  export { adjustMarginTool } from "./adjustMargin.js";
10
8
  export { closeAllPositionsTool } from "./closeAllPositions.js";
11
9
  export { checkApprovalTool } from "./checkApproval.js";
12
10
  export { getUserTradingFeeRateTool } from "./getUserTradingFeeRate.js";
13
11
  export { getNetworkFeeTool } from "./getNetworkFee.js";
12
+ export { checkAccountReadyTool } from "./checkAccountReady.js";
14
13
  // Tools — 市场数据
15
- export { getMarketPriceTool } from "./getMarketPrice.js";
16
- export { getOraclePriceTool } from "./getOraclePrice.js";
17
- export { searchMarketTool } from "./searchMarket.js";
18
- export { getKlineTool } from "./getKline.js";
19
- export { getKlineLatestBarTool } from "./getKlineLatestBar.js";
20
- export { getAllTickersTool } from "./getAllTickers.js";
21
- export { getMarketDetailTool, getPoolInfoTool, getLiquidityInfoTool } from "./marketInfo.js";
22
- export { getPoolListTool } from "./getPoolList.js";
23
- export { getPoolSymbolAllTool } from "./getPoolSymbolAll.js";
24
- export { getPoolLevelConfigTool } from "./poolConfig.js";
14
+ export { getPriceTool } from "./getPrice.js"; // Unified
15
+ export { getKlineTool } from "./getKline.js"; // Enhanced
16
+ export { listPoolsTool } from "./listPools.js"; // Unified
17
+ export { findPoolTool } from "./findPool.js"; // Unified
18
+ export { getPoolMetadataTool } from "./getPoolMetadata.js"; // Unified
25
19
  export { getBaseDetailTool } from "./getBaseDetail.js";
20
+ export { getAllTickersTool } from "./getAllTickers.js";
26
21
  // Tools — 池子 & 流动性
27
22
  export { createPerpMarketTool } from "./createPerpMarket.js";
28
23
  export { manageLiquidityTool, getLpPriceTool } from "./manageLiquidity.js";
29
24
  // Tools — 账户 & 查询
30
- export { getPositionsTool } from "./getPositions.js";
31
- export { getOpenOrdersTool, getOrderHistoryTool } from "./orderQueries.js";
32
- export { getPositionHistoryTool } from "./positionHistory.js";
33
- export { getAccountTool, getTradeFlowTool } from "./accountInfo.js";
25
+ export { getPositionsAllTool } from "./getPositionsAll.js"; // Unified
26
+ export { getOrdersTool } from "./getOrders.js"; // Unified
27
+ export { getAccountSnapshotTool } from "./getAccountSnapshot.js"; // Unified
28
+ export { getTradeFlowTool } from "./accountInfo.js"; // Kept for trade flow detail
34
29
  export { getMyLpHoldingsTool } from "./getMyLpHoldings.js";
35
- export { getAccountVipInfoTool } from "./getAccountVipInfo.js";
36
30
  export { accountDepositTool, accountWithdrawTool } from "./accountTransfer.js";
31
+ // Tools — 系统
32
+ export { searchToolsTool } from "./searchTools.js";
@@ -0,0 +1,53 @@
1
+ import { resolveClient } from "../auth/resolveClient.js";
2
+ import { getPoolList } from "../services/marketService.js";
3
+ function collectRows(input) {
4
+ if (Array.isArray(input))
5
+ return input.flatMap(collectRows);
6
+ if (!input || typeof input !== "object")
7
+ return [];
8
+ if (input.poolId || input.pool_id)
9
+ return [input];
10
+ return Object.values(input).flatMap(collectRows);
11
+ }
12
+ export const listPoolsTool = {
13
+ name: "list_pools",
14
+ description: "[MARKET] Get the complete list of all available tradable pools, including symbol and icon metadata.",
15
+ schema: {},
16
+ handler: async () => {
17
+ try {
18
+ const { client } = await resolveClient();
19
+ // Fetch both list and symbols
20
+ const [poolListRes, symbolsRes] = await Promise.all([
21
+ getPoolList(client),
22
+ client.markets.getPoolSymbolAll().catch(() => ({ data: [] }))
23
+ ]);
24
+ const poolsRaw = collectRows(poolListRes?.data ?? poolListRes);
25
+ const symbolsRaw = collectRows(symbolsRes?.data ?? symbolsRes);
26
+ const symbolMap = new Map(symbolsRaw
27
+ .filter((row) => row?.poolId || row?.pool_id)
28
+ .map((s) => [String(s.poolId ?? s.pool_id).toLowerCase(), s]));
29
+ const deduped = new Map();
30
+ for (const row of poolsRaw) {
31
+ const poolId = String(row?.poolId ?? row?.pool_id ?? "").trim().toLowerCase();
32
+ if (!poolId)
33
+ continue;
34
+ if (!deduped.has(poolId)) {
35
+ deduped.set(poolId, row);
36
+ }
37
+ }
38
+ const enriched = Array.from(deduped.values()).map(pool => {
39
+ const poolId = String(pool.poolId ?? pool.pool_id ?? "").toLowerCase();
40
+ const symbolData = symbolMap.get(poolId);
41
+ return {
42
+ ...pool,
43
+ icon: symbolData?.icon || null,
44
+ symbolName: symbolData?.symbolName || pool.symbolName || pool.baseQuoteSymbol || pool.symbol || null,
45
+ };
46
+ });
47
+ return { content: [{ type: "text", text: JSON.stringify({ status: "success", data: enriched }, (_, v) => typeof v === 'bigint' ? v.toString() : v, 2) }] };
48
+ }
49
+ catch (error) {
50
+ return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
51
+ }
52
+ },
53
+ };
@@ -47,9 +47,9 @@ function resolveLpAssetNames(detail) {
47
47
  }
48
48
  export const manageLiquidityTool = {
49
49
  name: "manage_liquidity",
50
- description: "Add or withdraw liquidity from a BASE or QUOTE pool. Success response includes LP naming metadata: base `mBASE.QUOTE`, quote `mQUOTE.BASE`, plus `operatedLpAssetName` based on poolType.",
50
+ description: "[LIQUIDITY] Add or withdraw liquidity from a BASE or QUOTE pool. Success response includes LP naming metadata: base `mBASE.QUOTE`, quote `mQUOTE.BASE`, plus `operatedLpAssetName` based on poolType.",
51
51
  schema: {
52
- action: z.enum(["deposit", "withdraw", "add", "remove", "increase", "decrease"]).describe("'deposit' or 'withdraw' (aliases: add, remove, increase, decrease)"),
52
+ action: z.coerce.string().describe("'deposit' or 'withdraw' (aliases: add/remove/increase/decrease; case-insensitive)"),
53
53
  poolType: z.enum(["BASE", "QUOTE"]).describe("'BASE' or 'QUOTE'"),
54
54
  poolId: z.string().describe("Pool ID or Base Token Address"),
55
55
  amount: z.coerce.number().positive().describe("Amount in human-readable units"),
@@ -62,6 +62,11 @@ export const manageLiquidityTool = {
62
62
  let { action, poolType, poolId } = args;
63
63
  const { amount, slippage } = args;
64
64
  const chainId = args.chainId ?? getChainId();
65
+ action = String(action ?? "").trim().toLowerCase();
66
+ const validActions = new Set(["deposit", "withdraw", "add", "remove", "increase", "decrease"]);
67
+ if (!validActions.has(action)) {
68
+ throw new Error(`Invalid action: ${args.action}. Use deposit/withdraw or aliases add/remove/increase/decrease.`);
69
+ }
65
70
  // 1. Action Alias Mapping
66
71
  if (action === "add" || action === "increase")
67
72
  action = "deposit";
@@ -74,7 +79,7 @@ export const manageLiquidityTool = {
74
79
  const detail = detailRes?.data || (detailRes?.marketId ? detailRes : null);
75
80
  if (!detail?.marketId) {
76
81
  throw new Error(`Pool ${poolId} not found on chainId ${chainId}. ` +
77
- `Please query a valid active pool via search_market/get_pool_list first.`);
82
+ `Please query a valid active pool via find_pool/list_pools first.`);
78
83
  }
79
84
  let raw;
80
85
  if (poolType === "QUOTE") {
@@ -88,7 +93,7 @@ export const manageLiquidityTool = {
88
93
  : await baseWithdraw(poolId, amount, slippage, chainId);
89
94
  }
90
95
  if (!raw) {
91
- throw new Error(`SDK returned an empty result for liquidity ${action}. This usually occurs if the pool is not in an Active state (state: 2) or if there is a contract-level restriction. Please check pool_info.`);
96
+ throw new Error(`SDK returned an empty result for liquidity ${action}. This usually occurs if the pool is not in an Active state (state: 2) or if there is a contract-level restriction. Please check get_pool_metadata.`);
92
97
  }
93
98
  if (raw && typeof raw === "object" && "code" in raw && Number(raw.code) !== 0) {
94
99
  throw new Error(`Liquidity ${action} failed: ${extractErrorMessage(raw)}`);
@@ -122,7 +127,7 @@ export const manageLiquidityTool = {
122
127
  };
123
128
  export const getLpPriceTool = {
124
129
  name: "get_lp_price",
125
- description: "Get the current internal net asset value (NAV) price of an LP token for a BASE or QUOTE pool. Note: This is NOT the underlying token's external Oracle market price (e.g. WETH's price), but rather the internal exchange rate / net worth of the LP token itself which fluctuates based on pool PnL and fees.",
130
+ description: "[LIQUIDITY] Get the current internal net asset value (NAV) price of an LP token for a BASE or QUOTE pool. Note: This is NOT the underlying token's external Oracle market price (e.g. WETH's price), but rather the internal exchange rate / net worth of the LP token itself which fluctuates based on pool PnL and fees.",
126
131
  schema: {
127
132
  poolType: z.enum(["BASE", "QUOTE"]).describe("'BASE' or 'QUOTE'"),
128
133
  poolId: z.string().describe("Pool ID"),
@@ -0,0 +1,163 @@
1
+ import { z } from "zod";
2
+ import { resolveClient, getChainId } from "../auth/resolveClient.js";
3
+ import { finalizeMutationResult } from "../utils/mutationResult.js";
4
+ function normalizeDirectionInput(value) {
5
+ if (value === undefined || value === null || value === "")
6
+ return undefined;
7
+ if (typeof value === "number") {
8
+ if (value === 0 || value === 1)
9
+ return value;
10
+ throw new Error("direction must be LONG/SHORT or 0/1.");
11
+ }
12
+ const text = String(value).trim().toUpperCase();
13
+ if (text === "0" || text === "LONG" || text === "BUY")
14
+ return 0;
15
+ if (text === "1" || text === "SHORT" || text === "SELL")
16
+ return 1;
17
+ throw new Error("direction must be LONG/SHORT or 0/1.");
18
+ }
19
+ function isNonEmpty(value) {
20
+ return value !== undefined && value !== null && String(value).trim().length > 0;
21
+ }
22
+ function readSnapshotText(snapshot, keys) {
23
+ for (const key of keys) {
24
+ const value = snapshot?.[key];
25
+ if (isNonEmpty(value))
26
+ return String(value).trim();
27
+ }
28
+ return "";
29
+ }
30
+ function normalizeUnitsInput(text) {
31
+ const raw = String(text ?? "").trim();
32
+ if (!raw)
33
+ return "";
34
+ if (/^(raw|human):/i.test(raw))
35
+ return raw;
36
+ if (/^\d+$/.test(raw))
37
+ return `raw:${raw}`;
38
+ return raw;
39
+ }
40
+ async function findOrderSnapshot(client, address, chainId, orderId, poolId) {
41
+ const target = String(orderId).toLowerCase();
42
+ try {
43
+ const openRes = await client.order.getOrders(address);
44
+ const openOrders = Array.isArray(openRes?.data) ? openRes.data : [];
45
+ const found = openOrders.find((order) => String(order?.orderId ?? order?.id ?? "").toLowerCase() === target);
46
+ if (found)
47
+ return found;
48
+ }
49
+ catch {
50
+ }
51
+ try {
52
+ const historyQuery = { chainId, limit: 50, page: 1 };
53
+ if (poolId)
54
+ historyQuery.poolId = poolId;
55
+ const historyRes = await client.order.getOrderHistory(historyQuery, address);
56
+ const historyOrders = Array.isArray(historyRes?.data) ? historyRes.data : [];
57
+ return historyOrders.find((order) => String(order?.orderId ?? order?.id ?? "").toLowerCase() === target) ?? null;
58
+ }
59
+ catch {
60
+ return null;
61
+ }
62
+ }
63
+ export const manageTpSlTool = {
64
+ name: "manage_tp_sl",
65
+ description: "[TRADE] Set or update Take Profit (TP) and Stop Loss (SL) for a position.",
66
+ schema: {
67
+ poolId: z.string().describe("Pool ID"),
68
+ positionId: z.string().optional().describe("Position ID (required for creating TP/SL on a position)"),
69
+ orderId: z.string().optional().describe("Existing TP/SL Order ID to update (optional)"),
70
+ size: z.union([z.string(), z.number()]).optional().describe("Order size (required for update if cannot be auto-resolved from order snapshot)"),
71
+ price: z.union([z.string(), z.number()]).optional().describe("Order price (required for update if cannot be auto-resolved from order snapshot)"),
72
+ tpPrice: z.union([z.string(), z.number()]).optional().describe("New TP price"),
73
+ tpSize: z.union([z.string(), z.number()]).optional().describe("New TP size"),
74
+ slPrice: z.union([z.string(), z.number()]).optional().describe("New SL price"),
75
+ slSize: z.union([z.string(), z.number()]).optional().describe("New SL size"),
76
+ direction: z.union([z.enum(["LONG", "SHORT"]), z.number().int()]).optional().describe("Position direction (LONG/SHORT or 0/1; required for new TP/SL)"),
77
+ leverage: z.number().optional().describe("Position leverage"),
78
+ executionFeeToken: z.string().optional().describe("Fee token address"),
79
+ slippagePct: z.union([z.string(), z.number()]).optional().describe("Slippage in 4dp raw units. Default 100 (1%)."),
80
+ useOrderCollateral: z.boolean().optional().describe("Whether updateOrderTpSl should use order collateral (default true)."),
81
+ chainId: z.number().int().positive().optional().describe("Optional chainId override"),
82
+ },
83
+ handler: async (args) => {
84
+ try {
85
+ const { client, address, signer } = await resolveClient();
86
+ const chainId = args.chainId ?? getChainId();
87
+ const direction = normalizeDirectionInput(args.direction);
88
+ const slippagePct = args.slippagePct ?? "100";
89
+ const { setPositionTpSl, updateOrderTpSl } = await import("../services/tradeService.js");
90
+ let raw;
91
+ if (args.orderId) {
92
+ // Update existing
93
+ const { getMarketDetail } = await import("../services/marketService.js");
94
+ const marketRes = await getMarketDetail(client, args.poolId, chainId);
95
+ const market = marketRes.data ?? marketRes;
96
+ const orderSnapshot = await findOrderSnapshot(client, address, chainId, args.orderId, args.poolId);
97
+ const snapshotSize = readSnapshotText(orderSnapshot, ["size", "orderSize", "positionSize"]);
98
+ const snapshotPrice = readSnapshotText(orderSnapshot, ["price", "orderPrice", "triggerPrice"]);
99
+ const snapshotTpPrice = readSnapshotText(orderSnapshot, ["tpPrice", "takeProfitPrice", "tpTriggerPrice"]);
100
+ const snapshotTpSize = readSnapshotText(orderSnapshot, ["tpSize", "takeProfitSize"]);
101
+ const snapshotSlPrice = readSnapshotText(orderSnapshot, ["slPrice", "stopLossPrice", "slTriggerPrice"]);
102
+ const snapshotSlSize = readSnapshotText(orderSnapshot, ["slSize", "stopLossSize"]);
103
+ const size = isNonEmpty(args.size) ? String(args.size) : snapshotSize;
104
+ const price = isNonEmpty(args.price) ? String(args.price) : snapshotPrice;
105
+ const tpPrice = isNonEmpty(args.tpPrice) ? String(args.tpPrice) : snapshotTpPrice;
106
+ const tpSize = isNonEmpty(args.tpSize) ? String(args.tpSize) : snapshotTpSize;
107
+ const slPrice = isNonEmpty(args.slPrice) ? String(args.slPrice) : snapshotSlPrice;
108
+ const slSize = isNonEmpty(args.slSize) ? String(args.slSize) : snapshotSlSize;
109
+ if (!size || !price) {
110
+ throw new Error("size and price are required for update. Provide them explicitly, or ensure orderId can be found via get_orders so they can be auto-resolved.");
111
+ }
112
+ if (!tpPrice && !slPrice) {
113
+ throw new Error("At least one of tpPrice or slPrice is required when updating TP/SL.");
114
+ }
115
+ if ((tpPrice && !tpSize) || (!tpPrice && tpSize)) {
116
+ throw new Error("TP update requires both tpPrice and tpSize (or resolvable existing TP fields).");
117
+ }
118
+ if ((slPrice && !slSize) || (!slPrice && slSize)) {
119
+ throw new Error("SL update requires both slPrice and slSize (or resolvable existing SL fields).");
120
+ }
121
+ raw = await updateOrderTpSl(client, address, {
122
+ orderId: args.orderId,
123
+ marketId: market.marketId,
124
+ poolId: args.poolId,
125
+ size: normalizeUnitsInput(size),
126
+ price: normalizeUnitsInput(price),
127
+ tpPrice: tpPrice ? normalizeUnitsInput(tpPrice) : "0",
128
+ tpSize: tpSize ? normalizeUnitsInput(tpSize) : "0",
129
+ slPrice: slPrice ? normalizeUnitsInput(slPrice) : "0",
130
+ slSize: slSize ? normalizeUnitsInput(slSize) : "0",
131
+ quoteToken: market.quoteToken,
132
+ useOrderCollateral: args.useOrderCollateral ?? true
133
+ }, chainId);
134
+ }
135
+ else {
136
+ // Create new
137
+ if (direction === undefined || !args.leverage) {
138
+ throw new Error("direction and leverage are required when creating new TP/SL.");
139
+ }
140
+ if (!args.positionId) {
141
+ throw new Error("positionId is required when creating new TP/SL.");
142
+ }
143
+ raw = await setPositionTpSl(client, address, {
144
+ poolId: args.poolId,
145
+ positionId: args.positionId,
146
+ direction,
147
+ leverage: args.leverage,
148
+ executionFeeToken: args.executionFeeToken,
149
+ slippagePct,
150
+ tpPrice: args.tpPrice,
151
+ tpSize: args.tpSize,
152
+ slPrice: args.slPrice,
153
+ slSize: args.slSize
154
+ }, chainId);
155
+ }
156
+ const data = await finalizeMutationResult(raw, signer, "manage_tp_sl");
157
+ return { content: [{ type: "text", text: JSON.stringify({ status: "success", data }, (_, v) => typeof v === 'bigint' ? v.toString() : v, 2) }] };
158
+ }
159
+ catch (error) {
160
+ return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
161
+ }
162
+ },
163
+ };
@@ -35,7 +35,7 @@ function pickMarketDetail(res) {
35
35
  }
36
36
  export const openPositionSimpleTool = {
37
37
  name: "open_position_simple",
38
- description: "High-level open position helper. Computes size/price/tradingFee and submits an increase order. Human units by default; use 'raw:' prefix for raw units.",
38
+ description: "[TRADE] High-level open position helper. Computes size/price/tradingFee and submits an increase order. Human units by default; use 'raw:' prefix for raw units.",
39
39
  schema: {
40
40
  poolId: z.string().optional().describe("Hex Pool ID. Provide either poolId or keyword."),
41
41
  keyword: z.string().optional().describe('Recommended: Market keyword, e.g. "BTC", "ETH", "XRP".'),
@@ -56,7 +56,7 @@ export const openPositionSimpleTool = {
56
56
  slippagePct: z.coerce
57
57
  .string()
58
58
  .optional()
59
- .describe(`${SLIPPAGE_PCT_4DP_DESC} Default 100 (=1%).`),
59
+ .describe(`${SLIPPAGE_PCT_4DP_DESC} Default 50 (=0.5%).`),
60
60
  postOnly: z.coerce.boolean().optional().describe("Post-only (default false)."),
61
61
  executionFeeToken: z.string().optional().describe("Execution fee token address (default quoteToken)."),
62
62
  assetClass: z.coerce.number().int().nonnegative().optional().describe("Fee query assetClass (default 1)."),
@@ -65,6 +65,10 @@ export const openPositionSimpleTool = {
65
65
  .string()
66
66
  .optional()
67
67
  .describe("Trading fee. e.g. '0.2' USDC or 'raw:...'. Default: computed via getUserTradingFeeRate."),
68
+ tpPrice: z.coerce.string().optional().describe("Take Profit trigger price."),
69
+ tpSize: z.coerce.string().optional().describe("Take Profit size (base units). If omitted but tpPrice set, uses full position size."),
70
+ slPrice: z.coerce.string().optional().describe("Stop Loss trigger price."),
71
+ slSize: z.coerce.string().optional().describe("Stop Loss size (base units). If omitted but slPrice set, uses full position size."),
68
72
  autoApprove: z.coerce.boolean().optional().describe("If true, auto-approve token spend (default false)."),
69
73
  approveMax: z.coerce.boolean().optional().describe("If autoApprove, approve MaxUint256 (default false)."),
70
74
  autoDeposit: z.coerce
@@ -118,7 +122,7 @@ export const openPositionSimpleTool = {
118
122
  }
119
123
  const orderType = mapOrderType(args.orderType ?? 0);
120
124
  const postOnly = Boolean(args.postOnly ?? false);
121
- const slippagePct = normalizeSlippagePct4dp(args.slippagePct ?? "100");
125
+ const slippagePct = normalizeSlippagePct4dp(args.slippagePct ?? "50");
122
126
  const executionFeeToken = normalizeAddress(args.executionFeeToken || quoteToken, "executionFeeToken");
123
127
  // 4) Determine reference price (30 decimals)
124
128
  let price30;
@@ -222,6 +226,10 @@ export const openPositionSimpleTool = {
222
226
  autoDeposit: Boolean(args.autoDeposit ?? false),
223
227
  priceMeta,
224
228
  sizeMeta,
229
+ tpPrice: args.tpPrice ? parseUserPrice30(args.tpPrice, "tpPrice") : null,
230
+ tpSize: args.tpSize ? parseUserUnits(args.tpSize, baseDecimals, "tpSize") : (args.tpPrice ? sizeRaw : null),
231
+ slPrice: args.slPrice ? parseUserPrice30(args.slPrice, "slPrice") : null,
232
+ slSize: args.slSize ? parseUserUnits(args.slSize, baseDecimals, "slSize") : (args.slPrice ? sizeRaw : null),
225
233
  };
226
234
  if (args.dryRun) {
227
235
  return {
@@ -254,7 +262,7 @@ export const openPositionSimpleTool = {
254
262
  }
255
263
  }
256
264
  // 8) Submit increase order using existing trade service
257
- const raw = await openPosition(client, address, {
265
+ const openArgs = {
258
266
  poolId,
259
267
  positionId: "",
260
268
  orderType,
@@ -271,7 +279,16 @@ export const openPositionSimpleTool = {
271
279
  tradingFee: `raw:${String(tradingFeeRaw)}`,
272
280
  marketId,
273
281
  autoDeposit: Boolean(args.autoDeposit ?? false),
274
- });
282
+ };
283
+ if (prep.tpPrice) {
284
+ openArgs.tpPrice = `raw:${prep.tpPrice}`;
285
+ openArgs.tpSize = `raw:${prep.tpSize}`;
286
+ }
287
+ if (prep.slPrice) {
288
+ openArgs.slPrice = `raw:${prep.slPrice}`;
289
+ openArgs.slSize = `raw:${prep.slSize}`;
290
+ }
291
+ const raw = await openPosition(client, address, openArgs);
275
292
  const data = await finalizeMutationResult(raw, signer, "open_position_simple");
276
293
  const txHash = data.confirmation?.txHash;
277
294
  const verification = txHash ? await verifyTradeOutcome(client, address, poolId, txHash) : null;
@@ -0,0 +1,35 @@
1
+ import { z } from "zod";
2
+ import * as allTools from "./index.js";
3
+ export const searchToolsTool = {
4
+ name: "search_tools",
5
+ description: "[UTILS] Search for available tools by keyword in their name or description.",
6
+ schema: {
7
+ keyword: z.string().describe("Keyword to search for in tool names or descriptions."),
8
+ },
9
+ handler: async (args) => {
10
+ try {
11
+ const keyword = args.keyword.toLowerCase();
12
+ const tools = Object.values(allTools);
13
+ const matches = tools
14
+ .filter(t => (t.name && t.name.toLowerCase().includes(keyword)) ||
15
+ (t.description && t.description.toLowerCase().includes(keyword)))
16
+ .map(t => ({
17
+ name: t.name,
18
+ description: t.description,
19
+ // We only return name and description to keep it concise
20
+ }));
21
+ if (matches.length === 0) {
22
+ return { content: [{ type: "text", text: `No tools found matching: ${args.keyword}` }] };
23
+ }
24
+ return {
25
+ content: [{
26
+ type: "text",
27
+ text: JSON.stringify({ status: "success", count: matches.length, tools: matches }, null, 2)
28
+ }]
29
+ };
30
+ }
31
+ catch (error) {
32
+ return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
33
+ }
34
+ },
35
+ };
@@ -50,13 +50,16 @@ export const getHistoryOrderStatusDesc = (status) => {
50
50
  * MarketPoolState
51
51
  */
52
52
  export const getMarketStateDesc = (state) => {
53
- switch (state) {
54
- case 0: return "Created";
55
- case 1: return "WaitOracle";
56
- case 2: return "Active";
57
- case 3: return "PreDelisting";
58
- case 4: return "Delisted";
59
- default: return `Unknown(${state})`;
53
+ if (state === undefined || state === null || state === "")
54
+ return "Unknown";
55
+ const s = Number(state);
56
+ switch (s) {
57
+ case 0: return "Created (Pending Setup)";
58
+ case 1: return "WaitOracle (Waiting for Price)";
59
+ case 2: return "Active (Tradable)";
60
+ case 3: return "PreDelisting (Closing Only)";
61
+ case 4: return "Delisted (Closed)";
62
+ default: return `Other(${s})`;
60
63
  }
61
64
  };
62
65
  /**
@@ -1,6 +1,7 @@
1
1
  const SLIPPAGE_PCT_4DP_RE = /^\d+$/;
2
2
  export const SLIPPAGE_PCT_4DP_MAX = 10000n;
3
3
  export const SLIPPAGE_PCT_4DP_DESC = "Slippage in 4-decimal precision raw units (1 = 0.01%, 10000 = 100%)";
4
+ const SLIPPAGE_PERCENT_HUMAN_RE = /^\d+(\.\d{1,2})?$/;
4
5
  export function isValidSlippagePct4dp(value) {
5
6
  if (!SLIPPAGE_PCT_4DP_RE.test(value))
6
7
  return false;
@@ -13,3 +14,25 @@ export function normalizeSlippagePct4dp(value, label = "slippagePct") {
13
14
  }
14
15
  return raw;
15
16
  }
17
+ export function normalizeSlippagePct4dpFlexible(value, label = "slippagePct") {
18
+ const raw = String(value ?? "").trim();
19
+ if (!raw) {
20
+ throw new Error(`${label} is required.`);
21
+ }
22
+ // Keep backward-compatible behavior: integer values remain raw 4dp units.
23
+ if (isValidSlippagePct4dp(raw)) {
24
+ return raw;
25
+ }
26
+ // Human percent helper: "1.0" / "1.25" / "1%"
27
+ const percentText = raw.endsWith("%") ? raw.slice(0, -1).trim() : raw;
28
+ if (!SLIPPAGE_PERCENT_HUMAN_RE.test(percentText)) {
29
+ throw new Error(`${label} must be raw 4dp integer (e.g. 100=1%) or human percent like "1.0" / "1%".`);
30
+ }
31
+ const [intPart, fracPart = ""] = percentText.split(".");
32
+ const frac2 = (fracPart + "00").slice(0, 2);
33
+ const converted = BigInt(intPart) * 100n + BigInt(frac2);
34
+ if (converted > SLIPPAGE_PCT_4DP_MAX) {
35
+ throw new Error(`${label} must be <= 100% (raw <= 10000).`);
36
+ }
37
+ return converted.toString();
38
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@michaleffffff/mcp-trading-server",
3
- "version": "3.0.3",
3
+ "version": "3.0.5",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "myx-mcp": "dist/server.js"