@michaleffffff/mcp-trading-server 2.8.2 → 2.9.2

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/dist/server.js CHANGED
@@ -81,7 +81,7 @@ function zodSchemaToJsonSchema(zodSchema) {
81
81
  };
82
82
  }
83
83
  // ─── MCP Server ───
84
- const server = new Server({ name: "myx-mcp-trading-server", version: "2.8.2" }, { capabilities: { tools: {}, resources: {}, prompts: {} } });
84
+ const server = new Server({ name: "myx-mcp-trading-server", version: "2.9.2" }, { capabilities: { tools: {}, resources: {}, prompts: {} } });
85
85
  // List tools
86
86
  server.setRequestHandler(ListToolsRequestSchema, async () => {
87
87
  return {
@@ -181,7 +181,7 @@ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
181
181
  async function main() {
182
182
  const transport = new StdioServerTransport();
183
183
  await server.connect(transport);
184
- logger.info("🚀 MYX Trading MCP Server v2.8.2 running (stdio, pure on-chain, prod ready)");
184
+ logger.info("🚀 MYX Trading MCP Server v2.9.2 running (stdio, pure on-chain, prod ready)");
185
185
  }
186
186
  main().catch((err) => {
187
187
  logger.error("Fatal Server Startup Error", err);
@@ -13,16 +13,24 @@ function resolveDirection(direction) {
13
13
  /**
14
14
  * 自动推断开启订单的触发类型 (Limit/Stop)
15
15
  */
16
- function resolveTriggerType(orderType, direction, triggerType) {
16
+ function resolveTriggerType(orderType, direction, isDecrease = false, triggerType) {
17
17
  if (triggerType !== undefined && triggerType !== null && triggerType !== 0) {
18
18
  return triggerType;
19
19
  }
20
- // LIMIT LONG: LTE(2), LIMIT SHORT: GTE(1)
20
+ // Opening: LIMIT LONG -> LTE(2), LIMIT SHORT -> GTE(1)
21
+ // Closing: LIMIT LONG -> GTE(1), LIMIT SHORT -> LTE(2)
21
22
  if (orderType === OrderType.LIMIT) {
23
+ if (isDecrease) {
24
+ return direction === 0 ? 1 : 2;
25
+ }
22
26
  return direction === 0 ? 2 : 1;
23
27
  }
24
- // STOP LONG: GTE(1), STOP SHORT: LTE(2)
28
+ // Opening: STOP LONG -> GTE(1), STOP SHORT -> LTE(2)
29
+ // Closing: STOP LONG -> LTE(2), STOP SHORT -> GTE(1)
25
30
  if (orderType === OrderType.STOP) {
31
+ if (isDecrease) {
32
+ return direction === 0 ? 2 : 1;
33
+ }
26
34
  return direction === 0 ? 1 : 2;
27
35
  }
28
36
  return 0; // MARKET order typically uses 0
@@ -138,7 +146,7 @@ export async function openPosition(client, address, args) {
138
146
  poolId: args.poolId,
139
147
  positionId: args.positionId,
140
148
  orderType: args.orderType,
141
- triggerType: resolveTriggerType(args.orderType, args.direction, args.triggerType),
149
+ triggerType: resolveTriggerType(args.orderType, args.direction, false, args.triggerType),
142
150
  direction: dir,
143
151
  collateralAmount: collateralRaw,
144
152
  size: sizeRaw,
@@ -184,7 +192,7 @@ export async function closePosition(client, address, args) {
184
192
  poolId: args.poolId,
185
193
  positionId: args.positionId,
186
194
  orderType: args.orderType,
187
- triggerType: resolveTriggerType(args.orderType, args.direction, args.triggerType),
195
+ triggerType: resolveTriggerType(args.orderType, args.direction, true, args.triggerType),
188
196
  direction: dir,
189
197
  collateralAmount: ensureUnits(args.collateralAmount, quoteDecimals, "collateralAmount"),
190
198
  size: ensureUnits(args.size, baseDecimals, "size"),
@@ -6,7 +6,7 @@ export const accountDepositTool = {
6
6
  name: "account_deposit",
7
7
  description: "Deposit funds from wallet into the MYX trading account.",
8
8
  schema: {
9
- amount: z.string().regex(/^\d+$/).describe("Amount to deposit (token raw units)"),
9
+ amount: z.union([z.string(), z.number()]).describe("Amount to deposit (human-readable or raw units)"),
10
10
  tokenAddress: z.string().describe("Token address"),
11
11
  },
12
12
  handler: async (args) => {
@@ -14,8 +14,13 @@ export const accountDepositTool = {
14
14
  const { client, signer } = await resolveClient();
15
15
  const chainId = getChainId();
16
16
  const tokenAddress = normalizeAddress(args.tokenAddress, "tokenAddress");
17
+ // For deposit, we default to quote decimals (6) as it's the most common use case.
18
+ // ensureUnits handles 'raw:' prefix if absolute precision is needed.
19
+ const { ensureUnits } = await import("../utils/units.js");
20
+ const { getQuoteDecimals } = await import("../auth/resolveClient.js");
21
+ const amount = ensureUnits(args.amount, getQuoteDecimals(), "amount");
17
22
  const raw = await client.account.deposit({
18
- amount: args.amount,
23
+ amount,
19
24
  tokenAddress,
20
25
  chainId,
21
26
  });
@@ -32,17 +37,22 @@ export const accountWithdrawTool = {
32
37
  description: "Withdraw funds from MYX trading account back to wallet.",
33
38
  schema: {
34
39
  poolId: z.string().describe("Pool ID to withdraw from"),
35
- amount: z.string().regex(/^\d+$/).describe("Amount to withdraw (token raw units)"),
36
- isQuoteToken: z.boolean().describe("Whether to withdraw as quote token"),
40
+ amount: z.union([z.string(), z.number()]).describe("Amount to withdraw (human-readable or raw units)"),
41
+ isQuoteToken: z.coerce.boolean().describe("Whether to withdraw as quote token"),
37
42
  },
38
43
  handler: async (args) => {
39
44
  try {
40
45
  const { client, address, signer } = await resolveClient();
41
46
  const chainId = getChainId();
47
+ const { ensureUnits } = await import("../utils/units.js");
48
+ const { getQuoteDecimals } = await import("../auth/resolveClient.js");
49
+ // Assuming 18 decimals for base and quoteDecimals for quote
50
+ const decimals = args.isQuoteToken ? getQuoteDecimals() : 18;
51
+ const amount = ensureUnits(args.amount, decimals, "amount");
42
52
  const raw = await client.account.withdraw({
43
53
  chainId,
44
54
  receiver: address,
45
- amount: args.amount,
55
+ amount,
46
56
  poolId: args.poolId,
47
57
  isQuoteToken: args.isQuoteToken,
48
58
  });
@@ -2,32 +2,45 @@ import { z } from "zod";
2
2
  import { resolveClient } from "../auth/resolveClient.js";
3
3
  import { closePosition as closePos } from "../services/tradeService.js";
4
4
  import { finalizeMutationResult } from "../utils/mutationResult.js";
5
- import { isValidSlippagePct4dp, SLIPPAGE_PCT_4DP_DESC } from "../utils/slippage.js";
5
+ import { SLIPPAGE_PCT_4DP_DESC } from "../utils/slippage.js";
6
6
  import { verifyTradeOutcome } from "../utils/verification.js";
7
+ import { mapDirection, mapOrderType, mapTriggerType } from "../utils/mappings.js";
7
8
  export const closePositionTool = {
8
9
  name: "close_position",
9
10
  description: "Create a decrease order using SDK-native parameters.",
10
11
  schema: {
11
12
  poolId: z.string().describe("Pool ID"),
12
13
  positionId: z.string().describe("Position ID to close"),
13
- orderType: z.coerce.number().int().min(0).max(3).describe("OrderType enum value"),
14
- triggerType: z.coerce.number().int().min(0).max(2).describe("TriggerType enum value"),
15
- direction: z.coerce.number().pipe(z.union([z.literal(0), z.literal(1)])).describe("0 = LONG, 1 = SHORT"),
16
- collateralAmount: z.union([z.string(), z.number()]).describe("Collateral amount (human-readable or raw units)"),
17
- size: z.union([z.string(), z.number()]).describe("Position size (human-readable or raw units)"),
18
- price: z.union([z.string(), z.number()]).describe("Price (human-readable or 30-dec raw units)"),
19
- timeInForce: z.coerce.number().int().describe("TimeInForce enum value"),
14
+ orderType: z.union([z.number(), z.string()]).describe("Order type: 0/MARKET or 1/LIMIT"),
15
+ triggerType: z.union([z.number(), z.string()]).optional().describe("Trigger type: 0/NONE, 1/GTE, 2/LTE"),
16
+ direction: z.union([z.number(), z.string()]).describe("Position direction: 0/LONG or 1/SHORT"),
17
+ collateralAmount: z.union([z.string(), z.number()]).describe("Collateral amount (human or raw units)"),
18
+ size: z.union([z.string(), z.number()]).describe("Position size (human or raw units)"),
19
+ price: z.union([z.string(), z.number()]).describe("Price (human or 30-dec raw units)"),
20
+ timeInForce: z.coerce.number().int().describe("TimeInForce: 0=GTC, 1=IOC, 2=FOK"),
20
21
  postOnly: z.coerce.boolean().describe("Post-only flag"),
21
- slippagePct: z.coerce.string().refine(isValidSlippagePct4dp, {
22
- message: "slippagePct must be an integer in [0, 10000] with 4-decimal precision (1 = 0.01%).",
23
- }).describe(SLIPPAGE_PCT_4DP_DESC),
22
+ slippagePct: z.coerce.string().default("50").describe(SLIPPAGE_PCT_4DP_DESC),
24
23
  executionFeeToken: z.string().describe("Execution fee token address"),
25
24
  leverage: z.coerce.number().describe("Leverage"),
26
25
  },
27
26
  handler: async (args) => {
28
27
  try {
29
28
  const { client, address, signer } = await resolveClient();
30
- const raw = await closePos(client, address, args);
29
+ const { chainId } = await resolveClient();
30
+ const poolId = args.poolId;
31
+ // Fetch pool detail to get quoteToken for execution fee
32
+ const poolResponse = await client.markets.getMarketDetail({ chainId, poolId });
33
+ const poolData = poolResponse?.data || (poolResponse?.marketId ? poolResponse : null);
34
+ if (!poolData)
35
+ throw new Error(`Could not find pool metadata for ID: ${poolId}`);
36
+ const mappedArgs = {
37
+ ...args,
38
+ direction: mapDirection(args.direction),
39
+ orderType: mapOrderType(args.orderType),
40
+ triggerType: args.triggerType !== undefined ? mapTriggerType(args.triggerType) : undefined,
41
+ executionFeeToken: poolData.quoteToken || args.executionFeeToken
42
+ };
43
+ const raw = await closePos(client, address, mappedArgs);
31
44
  const data = await finalizeMutationResult(raw, signer, "close_position");
32
45
  const txHash = data.confirmation?.txHash;
33
46
  let verification = null;
@@ -2,8 +2,9 @@ import { z } from "zod";
2
2
  import { resolveClient } from "../auth/resolveClient.js";
3
3
  import { openPosition } from "../services/tradeService.js";
4
4
  import { finalizeMutationResult } from "../utils/mutationResult.js";
5
- import { isValidSlippagePct4dp, SLIPPAGE_PCT_4DP_DESC } from "../utils/slippage.js";
5
+ import { SLIPPAGE_PCT_4DP_DESC } from "../utils/slippage.js";
6
6
  import { verifyTradeOutcome } from "../utils/verification.js";
7
+ import { mapDirection, mapOrderType, mapTriggerType } from "../utils/mappings.js";
7
8
  const POSITION_ID_RE = /^$|^0x[0-9a-fA-F]{64}$/;
8
9
  export const executeTradeTool = {
9
10
  name: "execute_trade",
@@ -13,30 +14,44 @@ export const executeTradeTool = {
13
14
  positionId: z.string().refine((value) => POSITION_ID_RE.test(value), {
14
15
  message: "positionId must be empty string for new position, or a bytes32 hex string.",
15
16
  }).describe("Position ID: Use empty string '' for NEW positions, or valid hex for INCREASING existing ones."),
16
- orderType: z.coerce.number().int().min(0).max(3).describe("0=Market, 1=Limit, 2=Stop, 3=StopLimit"),
17
- triggerType: z.coerce.number().int().min(0).max(2).describe("0=None (Market), 1=Price, 2=Trailing"),
18
- direction: z.coerce.number().pipe(z.union([z.literal(0), z.literal(1)])).describe("0 = LONG, 1 = SHORT"),
19
- collateralAmount: z.coerce.string().describe("Collateral. e.g. '100' or 'raw:100000000' (6 decimals for USDC)."),
20
- size: z.coerce.string().describe("Notional size in base tokens. e.g. '0.5' BTC or 'raw:50000000'."),
21
- price: z.coerce.string().describe("Execution or Limit price. e.g. '65000' or 'raw:65000000000000000000000000000000000' (30 decimals)."),
17
+ orderType: z.union([z.number(), z.string()]).describe("Market/Limit/Stop. e.g. 0 or 'MARKET'."),
18
+ triggerType: z.union([z.number(), z.string()]).optional().describe("0=None (Market), 1=GTE, 2=LTE. e.g. 'GTE'."),
19
+ direction: z.union([z.number(), z.string()]).describe("0/LONG/BUY or 1/SHORT/SELL."),
20
+ collateralAmount: z.union([z.string(), z.number()]).describe("Collateral. e.g. '100' or 'raw:100000000' (6 decimals for USDC)."),
21
+ size: z.union([z.string(), z.number()]).describe("Notional size in base tokens. e.g. '0.5' BTC or 'raw:50000000'."),
22
+ price: z.union([z.string(), z.number()]).describe("Execution or Limit price. e.g. '65000' or 'raw:...'"),
22
23
  timeInForce: z.coerce.number().int().describe("0=GTC, 1=IOC, 2=FOK"),
23
24
  postOnly: z.coerce.boolean().describe("If true, order only executes as Maker."),
24
- slippagePct: z.coerce.string().refine(isValidSlippagePct4dp, {
25
- message: "slippagePct must be an integer in [0, 10000] with 4-decimal precision (1 = 0.01%).",
26
- }).describe(`${SLIPPAGE_PCT_4DP_DESC}. Standard is 100 (1%).`),
25
+ slippagePct: z.coerce.string().default("50").describe(`${SLIPPAGE_PCT_4DP_DESC}. Default is 50 (0.5%).`),
27
26
  executionFeeToken: z.string().describe("Address of token to pay gas/execution fees (typically USDC)."),
28
27
  leverage: z.coerce.number().describe("Leverage multiplier, e.g., 10 for 10x."),
29
- tpSize: z.coerce.string().optional().describe("Take Profit size. Use '0' to disable."),
30
- tpPrice: z.coerce.string().optional().describe("Take Profit trigger price."),
31
- slSize: z.coerce.string().optional().describe("Stop Loss size. Use '0' to disable."),
32
- slPrice: z.coerce.string().optional().describe("Stop Loss trigger price."),
33
- tradingFee: z.coerce.string().describe("Estimated fee in raw units. Fetch via get_user_trading_fee_rate."),
28
+ tpSize: z.union([z.string(), z.number()]).optional().describe("Take Profit size. Use '0' to disable."),
29
+ tpPrice: z.union([z.string(), z.number()]).optional().describe("Take Profit trigger price."),
30
+ slSize: z.union([z.string(), z.number()]).optional().describe("Stop Loss size. Use '0' to disable."),
31
+ slPrice: z.union([z.string(), z.number()]).optional().describe("Stop Loss trigger price."),
32
+ tradingFee: z.union([z.string(), z.number()]).describe("Estimated fee in raw units. Fetch via get_user_trading_fee_rate."),
34
33
  marketId: z.string().describe("Specific Market Config Hash. Fetch via get_market_list."),
35
34
  },
36
35
  handler: async (args) => {
37
36
  try {
38
37
  const { client, address, signer } = await resolveClient();
39
- const raw = await openPosition(client, address, args);
38
+ const poolId = args.poolId;
39
+ // Fetch pool detail to get quoteToken for execution fee
40
+ const poolResponse = await client.markets.getMarketDetail({ chainId: await resolveClient().then(r => r.chainId), poolId });
41
+ const poolData = poolResponse?.data || (poolResponse?.marketId ? poolResponse : null);
42
+ if (!poolData)
43
+ throw new Error(`Could not find pool metadata for ID: ${poolId}`);
44
+ const mappedArgs = {
45
+ ...args,
46
+ direction: mapDirection(args.direction),
47
+ orderType: mapOrderType(args.orderType),
48
+ triggerType: args.triggerType !== undefined ? mapTriggerType(args.triggerType) : undefined,
49
+ // Normalize positionId
50
+ positionId: (args.positionId === "0" || !args.positionId) ? "" : args.positionId,
51
+ // Enforce executionFeeToken as quoteToken
52
+ executionFeeToken: poolData.quoteToken || args.executionFeeToken
53
+ };
54
+ const raw = await openPosition(client, address, mappedArgs);
40
55
  const data = await finalizeMutationResult(raw, signer, "execute_trade");
41
56
  const txHash = data.confirmation?.txHash;
42
57
  let verification = null;
@@ -5,7 +5,7 @@ export const getMarketListTool = {
5
5
  name: "get_market_list",
6
6
  description: "Get tradable markets/pools (state=2 Active). Supports configurable result limit; backend may still enforce its own cap.",
7
7
  schema: {
8
- limit: z.number().int().positive().optional().describe("Max results to request (default 1000)."),
8
+ limit: z.coerce.number().int().positive().optional().describe("Max results to request (default 1000)."),
9
9
  },
10
10
  handler: async (args) => {
11
11
  try {
@@ -7,13 +7,67 @@ export const getPositionsTool = {
7
7
  schema: {},
8
8
  handler: async () => {
9
9
  try {
10
- const { client, address } = await resolveClient();
10
+ const { client, address, chainId } = await resolveClient();
11
11
  const data = await getPositions(client, address);
12
12
  const positions = Array.isArray(data) ? data : (Array.isArray(data?.data) ? data.data : []);
13
- const enhancedData = positions.map((pos) => ({
14
- ...pos,
15
- directionDesc: getDirectionDesc(pos.direction)
13
+ if (positions.length === 0) {
14
+ return { content: [{ type: "text", text: JSON.stringify({ status: "success", data: [] }) }] };
15
+ }
16
+ // 1. Fetch Tickers for all relevant pools
17
+ const poolIds = [...new Set(positions.map((p) => p.poolId))];
18
+ const tickersRes = await client.markets.getTickerList({ chainId, poolIds });
19
+ const tickers = Array.isArray(tickersRes) ? tickersRes : (tickersRes?.data ?? []);
20
+ // 2. Fetch Pool Level Configs to get MM (Maintenance Margin Rate)
21
+ const configs = await Promise.all(poolIds.map(async (pid) => {
22
+ try {
23
+ const res = await client.markets.getPoolLevelConfig(pid, chainId);
24
+ return { poolId: pid, config: res?.levelConfig || res?.data?.levelConfig };
25
+ }
26
+ catch {
27
+ return { poolId: pid, config: null };
28
+ }
16
29
  }));
30
+ const enhancedData = positions.map((pos) => {
31
+ const ticker = tickers.find((t) => t.poolId === pos.poolId);
32
+ const currentPrice = Number(ticker?.price || 0);
33
+ const entryPrice = Number(pos.entryPrice || 0);
34
+ const size = Number(pos.size || 0);
35
+ const collateral = Number(pos.collateralAmount || 0);
36
+ const direction = pos.direction; // 0 = LONG, 1 = SHORT
37
+ const mm = configs.find(c => c.poolId === pos.poolId)?.config?.maintainCollateralRate || 0.02;
38
+ // Estimated PnL
39
+ let estimatedPnl = 0;
40
+ if (direction === 0) { // LONG
41
+ estimatedPnl = (currentPrice - entryPrice) * size;
42
+ }
43
+ else { // SHORT
44
+ estimatedPnl = (entryPrice - currentPrice) * size;
45
+ }
46
+ // ROI
47
+ const roi = collateral > 0 ? (estimatedPnl / collateral) * 100 : 0;
48
+ // Liquidation Price
49
+ // Long: (Entry * Size - Collateral) / (Size * (1 - MM))
50
+ // Short: (Entry * Size + Collateral) / (Size * (1 + MM))
51
+ let liqPrice = 0;
52
+ if (size > 0) {
53
+ if (direction === 0) {
54
+ liqPrice = (entryPrice * size - collateral) / (size * (1 - mm));
55
+ }
56
+ else {
57
+ liqPrice = (entryPrice * size + collateral) / (size * (1 + mm));
58
+ }
59
+ }
60
+ if (liqPrice < 0)
61
+ liqPrice = 0;
62
+ return {
63
+ ...pos,
64
+ directionDesc: getDirectionDesc(pos.direction),
65
+ currentPrice: currentPrice.toString(),
66
+ estimatedPnl: estimatedPnl.toFixed(4),
67
+ roi: roi.toFixed(2) + "%",
68
+ liquidationPrice: liqPrice.toFixed(4)
69
+ };
70
+ });
17
71
  return { content: [{ type: "text", text: JSON.stringify({ status: "success", data: enhancedData }, (_, v) => typeof v === 'bigint' ? v.toString() : v, 2) }] };
18
72
  }
19
73
  catch (error) {
@@ -4,9 +4,9 @@ export const getUserTradingFeeRateTool = {
4
4
  name: "get_user_trading_fee_rate",
5
5
  description: "Get maker/taker fee rates for a given assetClass and riskTier.",
6
6
  schema: {
7
- assetClass: z.number().int().nonnegative().describe("Asset class ID"),
8
- riskTier: z.number().int().nonnegative().describe("Risk tier"),
9
- chainId: z.number().int().positive().optional().describe("Optional chainId override"),
7
+ assetClass: z.coerce.number().int().nonnegative().describe("Asset class ID"),
8
+ riskTier: z.coerce.number().int().nonnegative().describe("Risk tier"),
9
+ chainId: z.coerce.number().int().positive().optional().describe("Optional chainId override"),
10
10
  },
11
11
  handler: async (args) => {
12
12
  try {
@@ -9,9 +9,9 @@ export const manageLiquidityTool = {
9
9
  action: z.enum(["deposit", "withdraw"]).describe("'deposit' or 'withdraw'"),
10
10
  poolType: z.enum(["BASE", "QUOTE"]).describe("'BASE' or 'QUOTE'"),
11
11
  poolId: z.string().describe("Pool ID"),
12
- amount: z.number().positive().describe("Amount in human-readable units"),
13
- slippage: z.number().min(0).describe("LP slippage ratio (e.g. 0.01 = 1%)"),
14
- chainId: z.number().int().positive().optional().describe("Optional chainId override"),
12
+ amount: z.coerce.number().positive().describe("Amount in human-readable units"),
13
+ slippage: z.coerce.number().min(0).describe("LP slippage ratio (e.g. 0.01 = 1%)"),
14
+ chainId: z.coerce.number().int().positive().optional().describe("Optional chainId override"),
15
15
  },
16
16
  handler: async (args) => {
17
17
  try {
@@ -29,6 +29,9 @@ export const manageLiquidityTool = {
29
29
  ? await baseDeposit(poolId, amount, slippage, args.chainId)
30
30
  : await baseWithdraw(poolId, amount, slippage, args.chainId);
31
31
  }
32
+ if (!raw) {
33
+ 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.`);
34
+ }
32
35
  const data = await finalizeMutationResult(raw, signer, "manage_liquidity");
33
36
  return { content: [{ type: "text", text: JSON.stringify({ status: "success", data }, (_, v) => typeof v === 'bigint' ? v.toString() : v, 2) }] };
34
37
  }
@@ -43,7 +46,7 @@ export const getLpPriceTool = {
43
46
  schema: {
44
47
  poolType: z.enum(["BASE", "QUOTE"]).describe("'BASE' or 'QUOTE'"),
45
48
  poolId: z.string().describe("Pool ID"),
46
- chainId: z.number().int().positive().optional().describe("Optional chainId override"),
49
+ chainId: z.coerce.number().int().positive().optional().describe("Optional chainId override"),
47
50
  },
48
51
  handler: async (args) => {
49
52
  try {
@@ -3,7 +3,7 @@ import { resolveClient } from "../auth/resolveClient.js";
3
3
  import { setPositionTpSl } from "../services/tradeService.js";
4
4
  import { finalizeMutationResult } from "../utils/mutationResult.js";
5
5
  import { mapDirection, mapTriggerType } from "../utils/mappings.js";
6
- import { isValidSlippagePct4dp, SLIPPAGE_PCT_4DP_DESC } from "../utils/slippage.js";
6
+ import { SLIPPAGE_PCT_4DP_DESC } from "../utils/slippage.js";
7
7
  export const setTpSlTool = {
8
8
  name: "set_tp_sl",
9
9
  description: "Create TP/SL order using SDK-native parameters.",
@@ -13,11 +13,9 @@ export const setTpSlTool = {
13
13
  direction: z.any().describe("0=LONG, 1=SHORT, or strings like 'BUY'/'SELL'/'LONG'/'SHORT'."),
14
14
  leverage: z.coerce.number().describe("Leverage multiplier, e.g., 10."),
15
15
  executionFeeToken: z.string().describe("Address of token to pay gas/execution fees."),
16
- tpTriggerType: z.union([z.number(), z.string()]).optional().describe("0=None, 1=Price (Market), 2=Trailing."),
17
- slTriggerType: z.union([z.number(), z.string()]).optional().describe("0=None, 1=Price (Market), 2=Trailing."),
18
- slippagePct: z.coerce.string().refine(isValidSlippagePct4dp, {
19
- message: "slippagePct must be an integer in [0, 10000] with 4-decimal precision (1 = 0.01%).",
20
- }).describe(`${SLIPPAGE_PCT_4DP_DESC}. Standard is 100 (1%).`),
16
+ tpTriggerType: z.union([z.number(), z.string()]).optional().describe("0/NONE, 1/GTE, 2/LTE. e.g. 'GTE'."),
17
+ slTriggerType: z.union([z.number(), z.string()]).optional().describe("0/NONE, 1/GTE, 2/LTE. e.g. 'LTE'."),
18
+ slippagePct: z.coerce.string().default("50").describe(`${SLIPPAGE_PCT_4DP_DESC}. Default is 50 (0.5%).`),
21
19
  tpPrice: z.coerce.string().optional().describe("Take Profit trigger price. e.g. '2.5' or 'raw:...' (30 decimals)."),
22
20
  tpSize: z.coerce.string().optional().describe("TP size in base tokens. Use '0' to disable."),
23
21
  slPrice: z.coerce.string().optional().describe("Stop Loss trigger price. e.g. '2.1' or 'raw:...' (30 decimals)."),
@@ -26,12 +24,20 @@ export const setTpSlTool = {
26
24
  handler: async (args) => {
27
25
  try {
28
26
  const { client, address, signer } = await resolveClient();
27
+ const { chainId } = await resolveClient();
28
+ const poolId = args.poolId;
29
+ // Fetch pool detail to get quoteToken for execution fee
30
+ const poolResponse = await client.markets.getMarketDetail({ chainId, poolId });
31
+ const poolData = poolResponse?.data || (poolResponse?.marketId ? poolResponse : null);
32
+ if (!poolData)
33
+ throw new Error(`Could not find pool metadata for ID: ${poolId}`);
29
34
  // Map inputs
30
35
  const mappedArgs = {
31
36
  ...args,
32
37
  direction: mapDirection(args.direction),
33
38
  tpTriggerType: args.tpTriggerType !== undefined ? mapTriggerType(args.tpTriggerType) : undefined,
34
39
  slTriggerType: args.slTriggerType !== undefined ? mapTriggerType(args.slTriggerType) : undefined,
40
+ executionFeeToken: poolData.quoteToken || args.executionFeeToken
35
41
  };
36
42
  const raw = await setPositionTpSl(client, address, mappedArgs);
37
43
  const data = await finalizeMutationResult(raw, signer, "set_tp_sl");
@@ -20,7 +20,7 @@ function normalizeDecimal(input) {
20
20
  return `${sign}${intPart || "0"}.${fracPart}`;
21
21
  }
22
22
  export function ensureUnits(value, decimals, label = "value") {
23
- const str = String(value).trim();
23
+ let str = String(value).trim();
24
24
  if (!str)
25
25
  throw new Error(`${label} is required.`);
26
26
  if (RAW_PREFIX_RE.test(str)) {
@@ -31,6 +31,14 @@ export function ensureUnits(value, decimals, label = "value") {
31
31
  }
32
32
  if (!DECIMAL_RE.test(str))
33
33
  throw new Error(`${label} must be a numeric string.`);
34
+ // Truncate decimals if they exceed the allowed precision to prevent parseUnits throwing
35
+ if (str.includes(".")) {
36
+ const parts = str.split(".");
37
+ if (parts[1].length > decimals) {
38
+ console.warn(`[ensureUnits] Truncating ${label} precision: ${str} -> ${parts[0]}.${parts[1].slice(0, decimals)}`);
39
+ str = `${parts[0]}.${parts[1].slice(0, decimals)}`;
40
+ }
41
+ }
34
42
  // If it's already a very large integer (e.g. > 12 digits or > decimals digits),
35
43
  // assume it's already in the smallest unit (Wei/Raw).
36
44
  if (!str.includes(".") && (str.length > 12 || str.length > decimals)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@michaleffffff/mcp-trading-server",
3
- "version": "2.8.2",
3
+ "version": "2.9.2",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "myx-mcp": "dist/server.js"