@michaleffffff/mcp-trading-server 2.7.0 → 2.9.0

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.
@@ -1 +1,2 @@
1
1
  export { tradeAnalysisPrompt } from "./tradeAnalysisPrompt.js";
2
+ export { tradingGuidePrompt } from "./tradingGuide.js";
@@ -20,7 +20,17 @@ export const tradeAnalysisPrompt = {
20
20
  role: "user",
21
21
  content: {
22
22
  type: "text",
23
- text: `Please analyze my current trading positions:\n\nPositions: ${JSON.stringify(positions, (_, v) => typeof v === 'bigint' ? v.toString() : v, 2)}\n\nMarket Context: ${args?.marketContext || "None provided"}\n\nWhat are the major risks and opportunities?`
23
+ text: `Analyze this user's trading portfolio with professional rigor:
24
+
25
+ Positions: ${JSON.stringify(positions, (_, v) => typeof v === 'bigint' ? v.toString() : v, 2)}
26
+
27
+ Market Context: ${args?.marketContext || "None provided"}
28
+
29
+ ## Analysis Requirements:
30
+ 1. **Risk Level**: Calculate current margin health and distance to liquidation price.
31
+ 2. **PnL Review**: Evaluate performance and identify if SL/TP are appropriately placed.
32
+ 3. **Actionable Suggestions**: Suggest specific size adjustments or TP/SL updates based on context.
33
+ 4. **Funding Outlook**: Brief comment on funding fee impacts if observable.`
24
34
  }
25
35
  }
26
36
  ]
@@ -0,0 +1,43 @@
1
+ import { resolveClient } from "../auth/resolveClient.js";
2
+ export const tradingGuidePrompt = {
3
+ name: "trading_best_practices",
4
+ description: "Get the gold standard workflow and parameter advice for using this MCP trading server.",
5
+ arguments: [],
6
+ run: async () => {
7
+ const { address, chainId } = await resolveClient();
8
+ return {
9
+ messages: [
10
+ {
11
+ role: "assistant",
12
+ content: {
13
+ type: "text",
14
+ text: `
15
+ # MYX Trading MCP Best Practices (v2.8.0)
16
+
17
+ You are an expert crypto trader using the MYX Protocol. To ensure successful execution and safe handling of user funds, follow these patterns:
18
+
19
+ ## 1. The Standard Workflow
20
+ 1. **Discovery**: Use \`search_market\` with a keyword (e.g., "BTC") to find the active \`poolId\`.
21
+ 2. **Context**: Use \`get_market_price\` and \`get_account_info\` to check the current market state and your available margin.
22
+ 3. **Execution**: Prefer \`open_position_simple\` for new trades. It handles unit conversions and pool resolution automatically.
23
+ 4. **Validation**: Always check the \`verification.verified\` flag in the output. If \`false\`, read the \`cancelReason\` to explain the failure to the user.
24
+
25
+ ## 2. Parameter Tips
26
+ - **Position IDs**: When opening a NEW position, \`positionId\` MUST be an empty string \`""\`.
27
+ - **Decimals**: Human-readable units (e.g., "0.1" BTC) are default for \`open_position_simple\`. SDK-native tools often require raw units; use the \`raw:\` prefix if you need forced precision.
28
+ - **Slippage**: Default is 100 (1%). For volatile meme tokens, consider 200-300 (2-3%).
29
+ - **Fees**: Use \`get_user_trading_fee_rate\` to estimate fees before large trades.
30
+
31
+ ## 3. Self-Healing
32
+ If a transaction reverts with a hex code, the server will attempt to decode it (e.g., "AccountInsufficientFreeAmount"). Inform the user specifically about what is missing rather than giving a generic error.
33
+
34
+ Current Session:
35
+ - Wallet: ${address}
36
+ - Chain ID: ${chainId}
37
+ `
38
+ }
39
+ }
40
+ ]
41
+ };
42
+ }
43
+ };
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.6.0" }, { capabilities: { tools: {}, resources: {}, prompts: {} } });
84
+ const server = new Server({ name: "myx-mcp-trading-server", version: "2.9.0" }, { 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.6.0 running (stdio, pure on-chain, prod ready)");
184
+ logger.info("🚀 MYX Trading MCP Server v2.9.0 running (stdio, pure on-chain, prod ready)");
185
185
  }
186
186
  main().catch((err) => {
187
187
  logger.error("Fatal Server Startup Error", err);
@@ -19,13 +19,18 @@ export const getAccountInfoTool = {
19
19
  structuredData = {
20
20
  ...result,
21
21
  data: {
22
- marginBalance: d[0],
23
- availableMargin: d[1],
24
- unrealizedPnL: d[2],
25
- initialMargin: d[3],
26
- maintenanceMargin: d[4],
27
- positionMargin: d[5],
28
- lastUpdateTime: d[6]
22
+ freeMargin: d[0],
23
+ walletBalance: d[1],
24
+ freeBaseAmount: d[2],
25
+ baseProfit: d[3],
26
+ quoteProfit: d[4],
27
+ reservedAmount: d[5],
28
+ releaseTime: d[6],
29
+ // Business Logic:
30
+ // tradeableMargin = freeMargin + walletBalance + (quoteProfit if releaseTime == 0)
31
+ tradeableMargin: (BigInt(d[0]) + BigInt(d[1]) + (BigInt(d[6]) === 0n ? BigInt(d[4]) : 0n)).toString(),
32
+ baseProfitStatus: "base token to be unlocked",
33
+ quoteProfitStatus: BigInt(d[6]) > 0n ? "quote token to be unlocked" : "quote token unlocked/available"
29
34
  }
30
35
  };
31
36
  }
@@ -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,37 @@ 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().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 mappedArgs = {
30
+ ...args,
31
+ direction: mapDirection(args.direction),
32
+ orderType: mapOrderType(args.orderType),
33
+ triggerType: args.triggerType !== undefined ? mapTriggerType(args.triggerType) : undefined,
34
+ };
35
+ const raw = await closePos(client, address, mappedArgs);
31
36
  const data = await finalizeMutationResult(raw, signer, "close_position");
32
37
  const txHash = data.confirmation?.txHash;
33
38
  let verification = null;
@@ -2,41 +2,46 @@ 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",
10
11
  description: "Create an increase order using SDK-native parameters.",
11
12
  schema: {
12
- poolId: z.string().describe("Pool ID"),
13
+ poolId: z.string().describe("Hex Pool ID, e.g. '0x14a19...'. Get via get_pool_list."),
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
- }).describe("Position ID: empty string for new position"),
16
- orderType: z.coerce.number().int().min(0).max(3).describe("OrderType enum value"),
17
- triggerType: z.coerce.number().int().min(0).max(2).describe("TriggerType enum value"),
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 amount (raw or human-readable)"),
20
- size: z.coerce.string().describe("Position size (raw or human-readable)"),
21
- price: z.coerce.string().describe("Price (raw or human-readable, 30 decimals)"),
22
- timeInForce: z.coerce.number().int().describe("TimeInForce enum value"),
23
- postOnly: z.coerce.boolean().describe("Post-only flag"),
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),
27
- executionFeeToken: z.string().describe("Execution fee token address"),
28
- leverage: z.coerce.number().describe("Leverage"),
29
- tpSize: z.coerce.string().optional().describe("TP size (raw or human-readable)"),
30
- tpPrice: z.coerce.string().optional().describe("TP price (raw or human-readable)"),
31
- slSize: z.coerce.string().optional().describe("SL size (raw or human-readable)"),
32
- slPrice: z.coerce.string().optional().describe("SL price (raw or human-readable)"),
33
- tradingFee: z.coerce.string().describe("Trading fee (raw units)"),
34
- marketId: z.string().describe("Market ID"),
16
+ }).describe("Position ID: Use empty string '' for NEW positions, or valid hex for INCREASING existing ones."),
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:...'"),
23
+ timeInForce: z.coerce.number().int().describe("0=GTC, 1=IOC, 2=FOK"),
24
+ postOnly: z.coerce.boolean().describe("If true, order only executes as Maker."),
25
+ slippagePct: z.coerce.string().describe(`${SLIPPAGE_PCT_4DP_DESC}. Standard is 100 (1%).`),
26
+ executionFeeToken: z.string().describe("Address of token to pay gas/execution fees (typically USDC)."),
27
+ leverage: z.coerce.number().describe("Leverage multiplier, e.g., 10 for 10x."),
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."),
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 mappedArgs = {
39
+ ...args,
40
+ direction: mapDirection(args.direction),
41
+ orderType: mapOrderType(args.orderType),
42
+ triggerType: args.triggerType !== undefined ? mapTriggerType(args.triggerType) : undefined,
43
+ };
44
+ const raw = await openPosition(client, address, mappedArgs);
40
45
  const data = await finalizeMutationResult(raw, signer, "execute_trade");
41
46
  const txHash = data.confirmation?.txHash;
42
47
  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 {
@@ -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 {
@@ -37,22 +37,22 @@ export const openPositionSimpleTool = {
37
37
  name: "open_position_simple",
38
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.",
39
39
  schema: {
40
- poolId: z.string().optional().describe("Pool ID. Provide either poolId or keyword."),
41
- keyword: z.string().optional().describe('Market keyword, e.g. "BTC". Provide either keyword or poolId.'),
42
- direction: z.any().describe("LONG (0), SHORT (1) or string BUY/SELL/LONG/SHORT"),
40
+ poolId: z.string().optional().describe("Hex Pool ID. Provide either poolId or keyword."),
41
+ keyword: z.string().optional().describe('Recommended: Market keyword, e.g. "BTC", "ETH", "XRP".'),
42
+ direction: z.any().describe("0=LONG, 1=SHORT, or strings like 'BUY'/'SELL'/'LONG'/'SHORT'."),
43
43
  collateralAmount: z.coerce
44
44
  .string()
45
- .describe("Collateral amount in quote token units (human by default; 'raw:' prefix for raw)."),
45
+ .describe("Collateral. e.g. '100' (quoted in USDC) or 'raw:100000000'."),
46
46
  leverage: z.coerce.number().int().positive().describe("Leverage (integer, e.g. 5, 10)."),
47
- orderType: z.union([z.string(), z.number()]).optional().describe("MARKET, LIMIT, STOP (default MARKET)."),
47
+ orderType: z.union([z.string(), z.number()]).optional().describe("MARKET, LIMIT, STOP (default MARKET). Strings allowed."),
48
48
  price: z.coerce
49
49
  .string()
50
50
  .optional()
51
- .describe("Price (human by default; 30-dec raw with 'raw:' prefix). Required for LIMIT/STOP."),
51
+ .describe("Price. e.g. '62000' or 'raw:...' (30 dec). Required for LIMIT/STOP."),
52
52
  size: z.coerce
53
53
  .string()
54
54
  .optional()
55
- .describe("Position size in base token units (human by default; 'raw:' prefix for raw). If omitted, computed from collateral*leverage/price."),
55
+ .describe("Position size. e.g. '0.5' BTC. If omitted, computed from collateral*leverage/price."),
56
56
  slippagePct: z.coerce
57
57
  .string()
58
58
  .optional()
@@ -64,13 +64,13 @@ export const openPositionSimpleTool = {
64
64
  tradingFee: z.coerce
65
65
  .string()
66
66
  .optional()
67
- .describe("Trading fee in quote token units (human by default; 'raw:' prefix for raw). If omitted, computed via getUserTradingFeeRate."),
68
- autoApprove: z.coerce.boolean().optional().describe("If true, auto-approve quote token spend when needed (default false)."),
69
- approveMax: z.coerce.boolean().optional().describe("If autoApprove, approve MaxUint256 (default false approves exact amount)."),
67
+ .describe("Trading fee. e.g. '0.2' USDC or 'raw:...'. Default: computed via getUserTradingFeeRate."),
68
+ autoApprove: z.coerce.boolean().optional().describe("If true, auto-approve token spend (default false)."),
69
+ approveMax: z.coerce.boolean().optional().describe("If autoApprove, approve MaxUint256 (default false)."),
70
70
  autoDeposit: z.coerce
71
71
  .boolean()
72
72
  .optional()
73
- .describe("If true, auto-deposit to margin account when marginBalance is insufficient (default false)."),
73
+ .describe("If true, auto-deposit to margin account if marginBalance < needed (default false)."),
74
74
  dryRun: z.coerce.boolean().optional().describe("If true, only compute params; do not send a transaction."),
75
75
  },
76
76
  handler: async (args) => {
@@ -8,20 +8,20 @@ export const setTpSlTool = {
8
8
  name: "set_tp_sl",
9
9
  description: "Create TP/SL order using SDK-native parameters.",
10
10
  schema: {
11
- poolId: z.string().describe("Pool ID"),
12
- positionId: z.string().describe("Position ID"),
13
- direction: z.any().describe("LONG (0), SHORT (1) or string BUY/SELL/LONG/SHORT"),
14
- leverage: z.coerce.number().describe("Leverage"),
15
- executionFeeToken: z.string().describe("Execution fee token address"),
16
- tpTriggerType: z.union([z.number(), z.string()]).optional().describe("TP trigger type (0=NONE, 1=GTE, 2=LTE)"),
17
- slTriggerType: z.union([z.number(), z.string()]).optional().describe("SL trigger type (0=NONE, 1=GTE, 2=LTE)"),
11
+ poolId: z.string().describe("Hex Pool ID, e.g. '0x14a19...'. Get via get_pool_list."),
12
+ positionId: z.string().describe("Active Position ID. Get via get_positions."),
13
+ direction: z.any().describe("0=LONG, 1=SHORT, or strings like 'BUY'/'SELL'/'LONG'/'SHORT'."),
14
+ leverage: z.coerce.number().describe("Leverage multiplier, e.g., 10."),
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/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
18
  slippagePct: z.coerce.string().refine(isValidSlippagePct4dp, {
19
19
  message: "slippagePct must be an integer in [0, 10000] with 4-decimal precision (1 = 0.01%).",
20
- }).describe(SLIPPAGE_PCT_4DP_DESC),
21
- tpPrice: z.coerce.string().optional().describe("TP price (raw or human-readable, 30 decimals)"),
22
- tpSize: z.coerce.string().optional().describe("TP size (raw or human-readable)"),
23
- slPrice: z.coerce.string().optional().describe("SL price (raw or human-readable, 30 decimals)"),
24
- slSize: z.coerce.string().optional().describe("SL size (raw or human-readable)"),
20
+ }).describe(`${SLIPPAGE_PCT_4DP_DESC}. Standard is 100 (1%).`),
21
+ tpPrice: z.coerce.string().optional().describe("Take Profit trigger price. e.g. '2.5' or 'raw:...' (30 decimals)."),
22
+ tpSize: z.coerce.string().optional().describe("TP size in base tokens. Use '0' to disable."),
23
+ slPrice: z.coerce.string().optional().describe("Stop Loss trigger price. e.g. '2.1' or 'raw:...' (30 decimals)."),
24
+ slSize: z.coerce.string().optional().describe("SL size in base tokens. Use '0' to disable."),
25
25
  },
26
26
  handler: async (args) => {
27
27
  try {
@@ -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.7.0",
3
+ "version": "2.9.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "myx-mcp": "dist/server.js"