@michaleffffff/mcp-trading-server 2.3.5 → 2.4.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.
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.3.5" }, { capabilities: { tools: {}, resources: {}, prompts: {} } });
84
+ const server = new Server({ name: "myx-mcp-trading-server", version: "2.4.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.3.5 running (stdio, pure on-chain, prod ready)");
184
+ logger.info("🚀 MYX Trading MCP Server v2.4.0 running (stdio, pure on-chain, prod ready)");
185
185
  }
186
186
  main().catch((err) => {
187
187
  logger.error("Fatal Server Startup Error", err);
@@ -1,15 +1,15 @@
1
1
  import { getChainId } from "../auth/resolveClient.js";
2
2
  import { getMarketStateDesc } from "../utils/mappings.js";
3
- export async function getMarketPrice(client, poolId) {
4
- const chainId = getChainId();
3
+ export async function getMarketPrice(client, poolId, chainIdOverride) {
4
+ const chainId = chainIdOverride ?? getChainId();
5
5
  const ticker = await client.markets.getTickerList({
6
6
  chainId,
7
7
  poolIds: [poolId],
8
8
  });
9
9
  return ticker?.data?.[0] ?? null;
10
10
  }
11
- export async function getOraclePrice(client, poolId) {
12
- const chainId = getChainId();
11
+ export async function getOraclePrice(client, poolId, chainIdOverride) {
12
+ const chainId = chainIdOverride ?? getChainId();
13
13
  return client.utils.getOraclePrice(poolId, chainId);
14
14
  }
15
15
  export async function searchMarket(client, keyword, limit = 1000) {
@@ -57,8 +57,8 @@ export async function searchMarket(client, keyword, limit = 1000) {
57
57
  };
58
58
  });
59
59
  }
60
- export async function getMarketDetail(client, poolId) {
61
- const chainId = getChainId();
60
+ export async function getMarketDetail(client, poolId, chainIdOverride) {
61
+ const chainId = chainIdOverride ?? getChainId();
62
62
  const res = await client.markets.getMarketDetail({ chainId, poolId });
63
63
  // Ensure it's returned as { data: ... } for consistency with other services if needed,
64
64
  // but looking at existing tools, they often stringify the whole result.
@@ -74,7 +74,7 @@ export async function getPoolList(client) {
74
74
  /**
75
75
  * 获取池子分级配置
76
76
  */
77
- export async function getPoolLevelConfig(client, poolId) {
78
- const chainId = getChainId();
77
+ export async function getPoolLevelConfig(client, poolId, chainIdOverride) {
78
+ const chainId = chainIdOverride ?? getChainId();
79
79
  return client.api.getPoolLevelConfig({ poolId, chainId });
80
80
  }
@@ -12,60 +12,63 @@ export async function createPool(baseToken, marketId) {
12
12
  /**
13
13
  * 获取池子信息
14
14
  */
15
- export async function getPoolInfo(poolId) {
16
- const chainId = getChainId();
15
+ export async function getPoolInfo(poolId, chainIdOverride) {
16
+ const chainId = chainIdOverride ?? getChainId();
17
17
  return pool.getPoolInfo(chainId, poolId);
18
18
  }
19
19
  /**
20
20
  * 获取池子详情
21
21
  */
22
- export async function getPoolDetail(poolId) {
23
- const chainId = getChainId();
22
+ export async function getPoolDetail(poolId, chainIdOverride) {
23
+ const chainId = chainIdOverride ?? getChainId();
24
24
  return pool.getPoolDetail(chainId, poolId);
25
25
  }
26
26
  /**
27
27
  * 获取流动性信息
28
28
  */
29
- export async function getLiquidityInfo(client, poolId, marketPrice) {
30
- const chainId = getChainId();
29
+ export async function getLiquidityInfo(client, poolId, marketPrice, chainIdOverride) {
30
+ const chainId = chainIdOverride ?? getChainId();
31
31
  const price30 = ensureUnits(marketPrice, 30, "marketPrice");
32
32
  return client.utils.getLiquidityInfo({ chainId, poolId, marketPrice: price30 });
33
33
  }
34
34
  /**
35
35
  * Quote 池 deposit
36
36
  */
37
- export async function quoteDeposit(poolId, amount, slippage) {
38
- const chainId = getChainId();
37
+ export async function quoteDeposit(poolId, amount, slippage, chainIdOverride) {
38
+ const chainId = chainIdOverride ?? getChainId();
39
39
  return quote.deposit({ chainId, poolId, amount, slippage });
40
40
  }
41
41
  /**
42
42
  * Quote 池 withdraw
43
43
  */
44
- export async function quoteWithdraw(poolId, amount, slippage) {
45
- const chainId = getChainId();
44
+ export async function quoteWithdraw(poolId, amount, slippage, chainIdOverride) {
45
+ const chainId = chainIdOverride ?? getChainId();
46
46
  return quote.withdraw({ chainId, poolId, amount, slippage });
47
47
  }
48
48
  /**
49
49
  * Base 池 deposit
50
50
  */
51
- export async function baseDeposit(poolId, amount, slippage) {
52
- const chainId = getChainId();
51
+ export async function baseDeposit(poolId, amount, slippage, chainIdOverride) {
52
+ const chainId = chainIdOverride ?? getChainId();
53
53
  return base.deposit({ chainId, poolId, amount, slippage });
54
54
  }
55
55
  /**
56
56
  * Base 池 withdraw
57
57
  */
58
- export async function baseWithdraw(poolId, amount, slippage) {
59
- const chainId = getChainId();
58
+ export async function baseWithdraw(poolId, amount, slippage, chainIdOverride) {
59
+ const chainId = chainIdOverride ?? getChainId();
60
60
  return base.withdraw({ chainId, poolId, amount, slippage });
61
61
  }
62
62
  /**
63
63
  * 获取 LP 价格
64
64
  */
65
- export async function getLpPrice(poolType, poolId) {
66
- const chainId = getChainId();
65
+ export async function getLpPrice(poolType, poolId, chainIdOverride) {
66
+ const chainId = chainIdOverride ?? getChainId();
67
67
  if (poolType === "BASE") {
68
68
  return base.getLpPrice(chainId, poolId);
69
69
  }
70
- return quote.getLpPrice(chainId, poolId);
70
+ if (poolType === "QUOTE") {
71
+ return quote.getLpPrice(chainId, poolId);
72
+ }
73
+ throw new Error("poolType must be 'BASE' or 'QUOTE'.");
71
74
  }
@@ -1,7 +1,7 @@
1
1
  import { Direction, OrderType, TriggerType } from "@myx-trade/sdk";
2
2
  import { parseUnits } from "ethers";
3
3
  import { getChainId, getQuoteToken, getQuoteDecimals } from "../auth/resolveClient.js";
4
- import { ensureUnits, parseHumanUnits } from "../utils/units.js";
4
+ import { ensureUnits } from "../utils/units.js";
5
5
  import { normalizeAddress } from "../utils/address.js";
6
6
  /**
7
7
  * 将人类可读数值转为指定精度的整数字串
@@ -35,7 +35,7 @@ export async function openPosition(client, address, args) {
35
35
  let calcPrice30;
36
36
  let orderPrice30;
37
37
  if (args.price) {
38
- const limitPrice30 = parseHumanUnits(args.price, 30, "price");
38
+ const limitPrice30 = ensureUnits(args.price, 30, "price");
39
39
  calcPrice30 = limitPrice30;
40
40
  orderPrice30 = limitPrice30;
41
41
  }
@@ -46,7 +46,7 @@ export async function openPosition(client, address, args) {
46
46
  console.error(`[Market Order] Fetched oracle price for size math: ${calcPrice30}`);
47
47
  }
48
48
  // 保证金转为 quote token 精度
49
- const collateralWei = parseHumanUnits(args.collateralAmount, quoteDecimals, "collateralAmount");
49
+ const collateralWei = ensureUnits(args.collateralAmount, quoteDecimals, "collateralAmount");
50
50
  // 仓位大小 (size): 在 MYX 这一版中,size 表示 Base Token(WETH)的数量,精度通常为 18
51
51
  // 计算公式:size = (collateral * leverage) / price
52
52
  // 为了防止浮点溢出,我们可以全部转为 BigInt 计算
@@ -85,11 +85,11 @@ export async function openPosition(client, address, args) {
85
85
  };
86
86
  // 可选止盈止损
87
87
  if (args.tpPrice) {
88
- orderParams.tpPrice = parseHumanUnits(args.tpPrice, 30, "tpPrice");
88
+ orderParams.tpPrice = ensureUnits(args.tpPrice, 30, "tpPrice");
89
89
  orderParams.tpSize = size;
90
90
  }
91
91
  if (args.slPrice) {
92
- orderParams.slPrice = parseHumanUnits(args.slPrice, 30, "slPrice");
92
+ orderParams.slPrice = ensureUnits(args.slPrice, 30, "slPrice");
93
93
  orderParams.slSize = size;
94
94
  }
95
95
  // tradingFee: SDK 第二个参数
@@ -111,7 +111,7 @@ export async function closePosition(client, address, args) {
111
111
  if (args.direction === undefined || args.direction === null) {
112
112
  throw new Error("direction is required (0=LONG, 1=SHORT), must match position direction.");
113
113
  }
114
- const price30 = parseHumanUnits(args.price, 30, "price");
114
+ const price30 = ensureUnits(args.price, 30, "price");
115
115
  const sizeWei = ensureUnits(args.size, 18, "size");
116
116
  const dir = resolveDirection(args.direction);
117
117
  return client.order.createDecreaseOrder({
@@ -125,6 +125,7 @@ export async function closePosition(client, address, args) {
125
125
  collateralAmount: "0",
126
126
  size: sizeWei,
127
127
  price: price30,
128
+ timeInForce: 0,
128
129
  postOnly: false,
129
130
  slippagePct: args.slippagePct ?? "100",
130
131
  executionFeeToken: quoteToken,
@@ -151,14 +152,14 @@ export async function setPositionTpSl(client, address, args) {
151
152
  executionFeeToken: quoteToken,
152
153
  tpTriggerType: args.direction === 0 ? TriggerType.GTE : TriggerType.LTE,
153
154
  slTriggerType: args.direction === 0 ? TriggerType.LTE : TriggerType.GTE,
154
- slippagePct: "100", // 默认 100 BPS = 1%
155
+ slippagePct: args.slippagePct ?? "100",
155
156
  };
156
157
  if (args.tpPrice) {
157
- params.tpPrice = parseHumanUnits(args.tpPrice, 30, "tpPrice");
158
+ params.tpPrice = ensureUnits(args.tpPrice, 30, "tpPrice");
158
159
  params.tpSize = args.tpSize || "0";
159
160
  }
160
161
  if (args.slPrice) {
161
- params.slPrice = parseHumanUnits(args.slPrice, 30, "slPrice");
162
+ params.slPrice = ensureUnits(args.slPrice, 30, "slPrice");
162
163
  params.slSize = args.slSize || "0";
163
164
  }
164
165
  return client.order.createPositionTpSlOrder(params);
@@ -173,7 +174,7 @@ export async function adjustMargin(client, address, args) {
173
174
  return client.position.adjustCollateral({
174
175
  poolId: args.poolId,
175
176
  positionId: args.positionId,
176
- adjustAmount: parseHumanUnits(args.adjustAmount, quoteDecimals, "adjustAmount"),
177
+ adjustAmount: ensureUnits(args.adjustAmount, quoteDecimals, "adjustAmount"),
177
178
  quoteToken,
178
179
  chainId,
179
180
  address,
@@ -234,14 +235,14 @@ export async function updateOrderTpSl(client, address, args) {
234
235
  const quoteToken = normalizeAddress(args.quoteToken || getQuoteToken(), "quoteToken");
235
236
  const params = {
236
237
  orderId: args.orderId,
237
- tpSize: args.tpSize || "0",
238
- tpPrice: args.tpPrice ? parseHumanUnits(args.tpPrice, 30, "tpPrice") : "0",
239
- slSize: args.slSize || "0",
240
- slPrice: args.slPrice ? parseHumanUnits(args.slPrice, 30, "slPrice") : "0",
238
+ tpSize: args.tpSize ? ensureUnits(args.tpSize, 18, "tpSize") : "0",
239
+ tpPrice: args.tpPrice ? ensureUnits(args.tpPrice, 30, "tpPrice") : "0",
240
+ slSize: args.slSize ? ensureUnits(args.slSize, 18, "slSize") : "0",
241
+ slPrice: args.slPrice ? ensureUnits(args.slPrice, 30, "slPrice") : "0",
241
242
  useOrderCollateral: args.useOrderCollateral ?? true,
242
243
  executionFeeToken: quoteToken,
243
- size: args.size || "0",
244
- price: args.price || "0",
244
+ size: args.size ? ensureUnits(args.size, 18, "size") : "0",
245
+ price: args.price ? ensureUnits(args.price, 30, "price") : "0",
245
246
  };
246
247
  return client.order.updateOrderTpSl(params, quoteToken, chainId, address, args.marketId, args.isTpSlOrder ?? true);
247
248
  }
@@ -40,14 +40,15 @@ export const getTradeFlowTool = {
40
40
  name: "get_trade_flow",
41
41
  description: "Get account trade flow / transaction history.",
42
42
  schema: {
43
- page: z.number().optional().describe("Page number (default 1)"),
44
- limit: z.number().optional().describe("Results per page (default 20)"),
43
+ poolId: z.string().optional().describe("Optional pool ID filter."),
44
+ page: z.coerce.number().optional().describe("Page number (default 1)"),
45
+ limit: z.coerce.number().optional().describe("Results per page (default 20)"),
45
46
  },
46
47
  handler: async (args) => {
47
48
  try {
48
49
  const { client, address } = await resolveClient();
49
50
  const chainId = getChainId();
50
- const query = { chainId, page: args.page ?? 1, limit: args.limit ?? 20 };
51
+ const query = { chainId, poolId: args.poolId, page: args.page ?? 1, limit: args.limit ?? 20 };
51
52
  const result = await client.account.getTradeFlow(query, address);
52
53
  const enhancedData = (result?.data || []).map((flow) => ({
53
54
  ...flow,
@@ -1,6 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { resolveClient, getChainId, getQuoteToken } from "../auth/resolveClient.js";
3
3
  import { normalizeAddress } from "../utils/address.js";
4
+ import { booleanLike } from "../utils/schema.js";
4
5
  export const accountDepositTool = {
5
6
  name: "account_deposit",
6
7
  description: "Deposit funds from wallet into the MYX trading account.",
@@ -31,7 +32,7 @@ export const accountWithdrawTool = {
31
32
  schema: {
32
33
  poolId: z.string().describe("Pool ID to withdraw from"),
33
34
  amount: z.string().describe("Amount to withdraw (in token decimals)"),
34
- isQuoteToken: z.boolean().optional().describe("Whether to withdraw as quote token (default true)"),
35
+ isQuoteToken: booleanLike.optional().describe("Whether to withdraw as quote token (default true)"),
35
36
  },
36
37
  handler: async (args) => {
37
38
  try {
@@ -7,7 +7,7 @@ export const adjustMarginTool = {
7
7
  schema: {
8
8
  poolId: z.string().describe("Pool ID"),
9
9
  positionId: z.string().describe("Position ID"),
10
- adjustAmount: z.string().describe("Amount in token decimals. Positive = add, negative = remove."),
10
+ adjustAmount: z.string().describe("SDK standard: quote token raw units. Positive = add, negative = remove. Compatibility: human-readable decimals are also accepted."),
11
11
  quoteToken: z.string().optional().describe("Quote token address"),
12
12
  },
13
13
  handler: async (args) => {
@@ -4,15 +4,16 @@ export const cancelOrderTool = {
4
4
  name: "cancel_order",
5
5
  description: "Cancel an open order by its order ID.",
6
6
  schema: {
7
- orderIds: z.array(z.string()).describe("Array of order IDs to cancel"),
7
+ orderId: z.string().optional().describe("Single order ID to cancel"),
8
+ orderIds: z.array(z.string()).optional().describe("Array of order IDs to cancel"),
8
9
  },
9
10
  handler: async (args) => {
10
11
  try {
11
12
  const { client } = await resolveClient();
12
13
  const chainId = getChainId();
13
- const ids = args.orderIds;
14
+ const ids = args.orderId ? [args.orderId] : (Array.isArray(args.orderIds) ? args.orderIds : []);
14
15
  if (!ids || ids.length === 0)
15
- throw new Error("orderIds array is required.");
16
+ throw new Error("orderId or orderIds is required.");
16
17
  let result;
17
18
  if (ids.length === 1) {
18
19
  result = await client.order.cancelOrder(ids[0], chainId);
@@ -1,6 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { resolveClient, getChainId, getQuoteToken } from "../auth/resolveClient.js";
3
3
  import { normalizeAddress } from "../utils/address.js";
4
+ import { booleanLike } from "../utils/schema.js";
4
5
  const MAX_UINT256 = "115792089237316195423570985008687907853269984665640564039457584007913129639935";
5
6
  export const checkApprovalTool = {
6
7
  name: "check_approval",
@@ -8,8 +9,8 @@ export const checkApprovalTool = {
8
9
  schema: {
9
10
  amount: z.string().describe("Amount to check approval for (in token decimals)"),
10
11
  quoteToken: z.string().optional().describe("Token address to check. Uses default if omitted."),
11
- autoApprove: z.boolean().optional().describe("If true, automatically approve when needed."),
12
- approveMax: z.boolean().optional().describe("If true with autoApprove, approve unlimited MaxUint256 (default false: approve exact amount only)."),
12
+ autoApprove: booleanLike.optional().describe("If true, automatically approve when needed."),
13
+ approveMax: booleanLike.optional().describe("If true with autoApprove, approve unlimited MaxUint256 (default false: approve exact amount only)."),
13
14
  },
14
15
  handler: async (args) => {
15
16
  try {
@@ -2,12 +2,13 @@ import { z } from "zod";
2
2
  import { resolveClient, getChainId, getQuoteToken } from "../auth/resolveClient.js";
3
3
  import { getOraclePrice } from "../services/marketService.js";
4
4
  import { ensureUnits } from "../utils/units.js";
5
+ import { bpsLikeString } from "../utils/schema.js";
5
6
  export const closeAllPositionsTool = {
6
7
  name: "close_all_positions",
7
8
  description: "Emergency: close ALL open positions in a pool at once. Use for risk management.",
8
9
  schema: {
9
10
  poolId: z.string().describe("Pool ID to close all positions in"),
10
- slippagePct: z.string().optional().describe("Slippage in bps (default '200' = 2% for emergency)"),
11
+ slippagePct: bpsLikeString.optional().describe("Slippage in bps (4-digit precision): 1 = 0.01%, default 200 = 2% for emergency"),
11
12
  },
12
13
  handler: async (args) => {
13
14
  try {
@@ -1,17 +1,20 @@
1
1
  import { z } from "zod";
2
2
  import { resolveClient } from "../auth/resolveClient.js";
3
3
  import { closePosition as closePos } from "../services/tradeService.js";
4
+ import { bpsLikeString } from "../utils/schema.js";
4
5
  export const closePositionTool = {
5
6
  name: "close_position",
6
7
  description: "Close an open position.",
7
8
  schema: {
8
9
  poolId: z.string().describe("Pool ID"),
9
10
  positionId: z.string().describe("Position ID to close"),
10
- direction: z.union([z.literal(0), z.literal(1)]).describe("0 = LONG, 1 = SHORT (must match position direction)"),
11
- leverage: z.number().describe("Position leverage"),
12
- size: z.string().describe("Size to close (human-readable or 18-decimal raw units)"),
13
- price: z.string().describe("Price in human-readable units"),
14
- slippagePct: z.string().optional().describe("Slippage in bps, default '100'"),
11
+ direction: z.coerce.number().int().refine((value) => value === 0 || value === 1, {
12
+ message: "direction must be 0 (LONG) or 1 (SHORT)",
13
+ }).describe("0 = LONG, 1 = SHORT (must match position direction)"),
14
+ leverage: z.coerce.number().describe("Position leverage"),
15
+ size: z.string().describe("SDK standard: 18-decimal raw size. Compatibility: human-readable decimals are also accepted."),
16
+ price: z.string().describe("SDK standard: 30-decimal raw price. Compatibility: human-readable decimals are also accepted."),
17
+ slippagePct: bpsLikeString.optional().describe("Slippage in bps (4-digit precision): 1 = 0.01%, default 100 = 1%"),
15
18
  quoteToken: z.string().optional().describe("Quote token address"),
16
19
  },
17
20
  handler: async (args) => {
@@ -1,21 +1,24 @@
1
1
  import { z } from "zod";
2
2
  import { resolveClient } from "../auth/resolveClient.js";
3
3
  import { openPosition } from "../services/tradeService.js";
4
+ import { bpsLikeString } from "../utils/schema.js";
4
5
  export const executeTradeTool = {
5
6
  name: "execute_trade",
6
7
  description: "Execute a new trade or add to an existing position.",
7
8
  schema: {
8
9
  poolId: z.string().describe("Pool ID to trade in"),
9
- direction: z.union([z.literal(0), z.literal(1)]).describe("0 = LONG, 1 = SHORT"),
10
- collateralAmount: z.string().describe("Collateral in human-readable units (e.g. '100' for 100 USDC)"),
11
- leverage: z.number().optional().describe("Leverage multiplier (e.g. 10, default 10)"),
12
- price: z.string().optional().describe("Limit price in human-readable units (e.g. '3000'). Required for LIMIT orders, omitted for MARKET orders."),
10
+ direction: z.coerce.number().int().refine((value) => value === 0 || value === 1, {
11
+ message: "direction must be 0 (LONG) or 1 (SHORT)",
12
+ }).describe("0 = LONG, 1 = SHORT"),
13
+ collateralAmount: z.string().describe("SDK standard: quote token raw units. Compatibility: human-readable decimals are also accepted."),
14
+ leverage: z.coerce.number().optional().describe("Leverage multiplier (e.g. 10, default 10)"),
15
+ price: z.string().optional().describe("SDK standard: 30-decimal raw price for LIMIT orders. Compatibility: human-readable decimals are also accepted."),
13
16
  quoteToken: z.string().optional().describe("Quote token address. Uses env default if omitted."),
14
- quoteDecimals: z.number().optional().describe("Quote token decimals (default 6)."),
15
- slippagePct: z.string().optional().describe("Slippage in bps, default '100' = 1%"),
17
+ quoteDecimals: z.coerce.number().optional().describe("Quote token decimals (default 6)."),
18
+ slippagePct: bpsLikeString.optional().describe("Slippage in bps (4-digit precision): 1 = 0.01%, default 100 = 1%"),
16
19
  positionId: z.string().optional().describe("Existing position ID to add to. Default '0' = new position."),
17
- tpPrice: z.string().optional().describe("Take-profit price (human-readable)"),
18
- slPrice: z.string().optional().describe("Stop-loss price (human-readable)"),
20
+ tpPrice: z.string().optional().describe("SDK standard: 30-decimal raw TP price. Compatibility: human-readable decimals are also accepted."),
21
+ slPrice: z.string().optional().describe("SDK standard: 30-decimal raw SL price. Compatibility: human-readable decimals are also accepted."),
19
22
  },
20
23
  handler: async (args) => {
21
24
  try {
@@ -4,7 +4,7 @@ export const getAccountVipInfoTool = {
4
4
  name: "get_account_vip_info",
5
5
  description: "Get account VIP/fee-tier information.",
6
6
  schema: {
7
- chainId: z.number().optional().describe("Optional chainId override"),
7
+ chainId: z.coerce.number().optional().describe("Optional chainId override"),
8
8
  },
9
9
  handler: async (args) => {
10
10
  try {
@@ -1,17 +1,71 @@
1
1
  import { z } from "zod";
2
2
  import { resolveClient, getChainId } from "../auth/resolveClient.js";
3
+ function normalizeLower(value) {
4
+ return String(value ?? "").trim().toLowerCase();
5
+ }
6
+ function collectRows(input) {
7
+ if (Array.isArray(input)) {
8
+ return input.flatMap((item) => collectRows(item));
9
+ }
10
+ if (!input || typeof input !== "object") {
11
+ return [];
12
+ }
13
+ if (input.poolId || input.pool_id) {
14
+ return [input];
15
+ }
16
+ return Object.values(input).flatMap((value) => collectRows(value));
17
+ }
18
+ function findPoolIdByBaseAddress(rows, baseAddress) {
19
+ const target = normalizeLower(baseAddress);
20
+ if (!target)
21
+ return null;
22
+ for (const row of rows) {
23
+ const candidates = [
24
+ row.baseToken,
25
+ row.base_token,
26
+ row.baseAddress,
27
+ row.base_address,
28
+ row.base,
29
+ row.tokenAddress,
30
+ row.token_address,
31
+ ];
32
+ const hit = candidates.some((candidate) => normalizeLower(candidate) === target);
33
+ if (!hit)
34
+ continue;
35
+ const poolId = row.poolId || row.pool_id;
36
+ if (poolId) {
37
+ return String(poolId);
38
+ }
39
+ }
40
+ return null;
41
+ }
3
42
  export const getBaseDetailTool = {
4
43
  name: "get_base_detail",
5
- description: "Get base asset detail metrics for a pool.",
44
+ description: "Get base asset detail metrics. SDK doc uses baseAddress; MCP also accepts poolId for compatibility.",
6
45
  schema: {
7
- poolId: z.string().describe("Pool ID"),
8
- chainId: z.number().optional().describe("Optional chainId override"),
46
+ baseAddress: z.string().optional().describe("SDK doc parameter: base token address."),
47
+ poolId: z.string().optional().describe("Compatibility parameter: pool ID."),
48
+ chainId: z.coerce.number().optional().describe("Optional chainId override"),
9
49
  },
10
50
  handler: async (args) => {
11
51
  try {
12
52
  const { client } = await resolveClient();
13
53
  const chainId = args.chainId ?? getChainId();
14
- const result = await client.markets.getBaseDetail({ chainId, poolId: args.poolId });
54
+ let poolId = args.poolId;
55
+ if (!poolId && args.baseAddress) {
56
+ const sources = await Promise.allSettled([
57
+ client.api.getPoolList(),
58
+ client.api.getMarketList(),
59
+ ]);
60
+ const rows = sources
61
+ .filter((result) => result.status === "fulfilled")
62
+ .flatMap((result) => collectRows(result.value));
63
+ poolId = findPoolIdByBaseAddress(rows, args.baseAddress) || undefined;
64
+ }
65
+ if (!poolId) {
66
+ throw new Error("poolId is required. If using baseAddress, no matching pool was found.");
67
+ }
68
+ const result = await client.markets.getBaseDetail({ chainId, poolId });
15
69
  return { content: [{ type: "text", text: JSON.stringify({ status: "success", data: result }, (_, v) => typeof v === "bigint" ? v.toString() : v, 2) }] };
16
70
  }
17
71
  catch (error) {
@@ -6,7 +6,7 @@ export const getKlineTool = {
6
6
  schema: {
7
7
  poolId: z.string().describe("Pool ID"),
8
8
  interval: z.string().describe("K-line interval: '1m','5m','15m','30m','1h','4h','1d','1w','1M'"),
9
- limit: z.number().optional().describe("Number of bars (default 100)"),
9
+ limit: z.coerce.number().optional().describe("Number of bars (default 100)"),
10
10
  },
11
11
  handler: async (args) => {
12
12
  try {
@@ -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().optional().describe("Max results to request (default 1000)."),
8
+ limit: z.coerce.number().optional().describe("Max results to request (default 1000)."),
9
9
  },
10
10
  handler: async (args) => {
11
11
  try {
@@ -1,16 +1,18 @@
1
1
  import { z } from "zod";
2
- import { resolveClient } from "../auth/resolveClient.js";
2
+ import { resolveClient, getChainId } from "../auth/resolveClient.js";
3
3
  import { getMarketPrice } from "../services/marketService.js";
4
4
  export const getMarketPriceTool = {
5
5
  name: "get_market_price",
6
6
  description: "Get the current market price for a specific pool.",
7
7
  schema: {
8
8
  poolId: z.string().describe("Pool ID to get price for"),
9
+ chainId: z.coerce.number().optional().describe("Optional chainId override"),
9
10
  },
10
11
  handler: async (args) => {
11
12
  try {
12
13
  const { client } = await resolveClient();
13
- const data = await getMarketPrice(client, args.poolId);
14
+ const chainId = args.chainId ?? getChainId();
15
+ const data = await getMarketPrice(client, args.poolId, chainId);
14
16
  return { content: [{ type: "text", text: JSON.stringify({ status: "success", data }, (k, v) => typeof v === 'bigint' ? v.toString() : v, 2) }] };
15
17
  }
16
18
  catch (error) {
@@ -5,7 +5,7 @@ export const getNetworkFeeTool = {
5
5
  description: "Estimate network fee requirements for a market.",
6
6
  schema: {
7
7
  marketId: z.string().describe("Market ID"),
8
- chainId: z.number().optional().describe("Optional chainId override"),
8
+ chainId: z.coerce.number().optional().describe("Optional chainId override"),
9
9
  },
10
10
  handler: async (args) => {
11
11
  try {
@@ -1,16 +1,18 @@
1
1
  import { z } from "zod";
2
- import { resolveClient } from "../auth/resolveClient.js";
2
+ import { resolveClient, getChainId } from "../auth/resolveClient.js";
3
3
  import { getOraclePrice } from "../services/marketService.js";
4
4
  export const getOraclePriceTool = {
5
5
  name: "get_oracle_price",
6
6
  description: "Get the current oracle price for a specific pool.",
7
7
  schema: {
8
8
  poolId: z.string().describe("Pool ID to get oracle price for"),
9
+ chainId: z.coerce.number().optional().describe("Optional chainId override"),
9
10
  },
10
11
  handler: async (args) => {
11
12
  try {
12
13
  const { client } = await resolveClient();
13
- const data = await getOraclePrice(client, args.poolId);
14
+ const chainId = args.chainId ?? getChainId();
15
+ const data = await getOraclePrice(client, args.poolId, chainId);
14
16
  return { content: [{ type: "text", text: JSON.stringify({ status: "success", data }, (k, v) => typeof v === 'bigint' ? v.toString() : v, 2) }] };
15
17
  }
16
18
  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().describe("Asset class ID"),
8
- riskTier: z.number().describe("Risk tier"),
9
- chainId: z.number().optional().describe("Optional chainId override"),
7
+ assetClass: z.coerce.number().describe("Asset class ID"),
8
+ riskTier: z.coerce.number().describe("Risk tier"),
9
+ chainId: z.coerce.number().optional().describe("Optional chainId override"),
10
10
  },
11
11
  handler: async (args) => {
12
12
  try {
@@ -1,25 +1,27 @@
1
1
  import { z } from "zod";
2
2
  import { quoteDeposit, quoteWithdraw, baseDeposit, baseWithdraw, getLpPrice, } from "../services/poolService.js";
3
3
  import { parseSafeNumber } from "../utils/units.js";
4
+ import { lpSlippageLike } from "../utils/schema.js";
4
5
  export const manageLiquidityTool = {
5
6
  name: "manage_liquidity",
6
- description: "Add or withdraw liquidity from a BASE or QUOTE pool. Amount is in human-readable units (e.g. 2000 for USDC, 0.01 for ETH).",
7
+ description: "Add or withdraw liquidity from a BASE or QUOTE pool. Amount is in human-readable units. SDK-standard slippage is ratio (0.01 = 1%), and BPS input is auto-compatible.",
7
8
  schema: {
8
- action: z.string().describe("'deposit' or 'withdraw'"),
9
- poolType: z.string().describe("'BASE' or 'QUOTE'"),
9
+ action: z.string().trim().toLowerCase().refine((value) => value === "deposit" || value === "withdraw", {
10
+ message: "action must be 'deposit' or 'withdraw'.",
11
+ }).describe("'deposit' or 'withdraw'"),
12
+ poolType: z.string().trim().toUpperCase().refine((value) => value === "BASE" || value === "QUOTE", {
13
+ message: "poolType must be 'BASE' or 'QUOTE'.",
14
+ }).describe("'BASE' or 'QUOTE'"),
10
15
  poolId: z.string().describe("Pool ID"),
11
16
  amount: z.union([z.number(), z.string()]).describe("Amount in human-readable units (e.g. 2000 for USDC, 0.01 for ETH)"),
12
- slippage: z.union([z.number(), z.string()]).describe("Slippage tolerance (e.g. 0.01 = 1%)"),
17
+ slippage: lpSlippageLike.describe("SDK-standard LP slippage ratio (0.01 = 1%). BPS input is also accepted for compatibility (100 => 1%)."),
18
+ chainId: z.coerce.number().optional().describe("Optional chainId override"),
13
19
  },
14
20
  handler: async (args) => {
15
21
  try {
16
22
  const { action, poolType, poolId } = args;
17
23
  const amount = parseSafeNumber(args.amount, "amount");
18
- const slippage = parseSafeNumber(args.slippage, "slippage");
19
- if (!["deposit", "withdraw"].includes(action))
20
- throw new Error("action must be 'deposit' or 'withdraw'.");
21
- if (!["BASE", "QUOTE"].includes(poolType))
22
- throw new Error("poolType must be 'BASE' or 'QUOTE'.");
24
+ const slippage = args.slippage;
23
25
  if (amount <= 0)
24
26
  throw new Error("amount must be a positive number.");
25
27
  if (slippage < 0)
@@ -27,13 +29,13 @@ export const manageLiquidityTool = {
27
29
  let result;
28
30
  if (poolType === "QUOTE") {
29
31
  result = action === "deposit"
30
- ? await quoteDeposit(poolId, amount, slippage)
31
- : await quoteWithdraw(poolId, amount, slippage);
32
+ ? await quoteDeposit(poolId, amount, slippage, args.chainId)
33
+ : await quoteWithdraw(poolId, amount, slippage, args.chainId);
32
34
  }
33
35
  else {
34
36
  result = action === "deposit"
35
- ? await baseDeposit(poolId, amount, slippage)
36
- : await baseWithdraw(poolId, amount, slippage);
37
+ ? await baseDeposit(poolId, amount, slippage, args.chainId)
38
+ : await baseWithdraw(poolId, amount, slippage, args.chainId);
37
39
  }
38
40
  return { content: [{ type: "text", text: JSON.stringify({ status: "success", data: result }, (_, v) => typeof v === 'bigint' ? v.toString() : v, 2) }] };
39
41
  }
@@ -46,12 +48,15 @@ export const getLpPriceTool = {
46
48
  name: "get_lp_price",
47
49
  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.",
48
50
  schema: {
49
- poolType: z.string().describe("'BASE' or 'QUOTE'"),
51
+ poolType: z.string().trim().toUpperCase().refine((value) => value === "BASE" || value === "QUOTE", {
52
+ message: "poolType must be 'BASE' or 'QUOTE'.",
53
+ }).describe("'BASE' or 'QUOTE'"),
50
54
  poolId: z.string().describe("Pool ID"),
55
+ chainId: z.coerce.number().optional().describe("Optional chainId override"),
51
56
  },
52
57
  handler: async (args) => {
53
58
  try {
54
- const data = await getLpPrice(args.poolType, args.poolId);
59
+ const data = await getLpPrice(args.poolType, args.poolId, args.chainId);
55
60
  return { content: [{ type: "text", text: JSON.stringify({ status: "success", data }, (k, v) => typeof v === 'bigint' ? v.toString() : v, 2) }] };
56
61
  }
57
62
  catch (error) {
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { resolveClient } from "../auth/resolveClient.js";
2
+ import { resolveClient, getChainId } from "../auth/resolveClient.js";
3
3
  import { getMarketDetail } from "../services/marketService.js";
4
4
  import { getPoolInfo, getLiquidityInfo } from "../services/poolService.js";
5
5
  export const getMarketDetailTool = {
@@ -7,11 +7,13 @@ export const getMarketDetailTool = {
7
7
  description: "Get detailed information for a specific trading pool (fee rates, open interest, etc.).",
8
8
  schema: {
9
9
  poolId: z.string().describe("Pool ID"),
10
+ chainId: z.coerce.number().optional().describe("Optional chainId override"),
10
11
  },
11
12
  handler: async (args) => {
12
13
  try {
13
14
  const { client } = await resolveClient();
14
- const data = await getMarketDetail(client, args.poolId);
15
+ const chainId = args.chainId ?? getChainId();
16
+ const data = await getMarketDetail(client, args.poolId, chainId);
15
17
  return { content: [{ type: "text", text: JSON.stringify({ status: "success", data }, (_, v) => typeof v === 'bigint' ? v.toString() : v, 2) }] };
16
18
  }
17
19
  catch (error) {
@@ -24,10 +26,12 @@ export const getPoolInfoTool = {
24
26
  description: "Get pool on-chain information (reserves, utilization, etc.).",
25
27
  schema: {
26
28
  poolId: z.string().describe("Pool ID"),
29
+ chainId: z.coerce.number().optional().describe("Optional chainId override"),
27
30
  },
28
31
  handler: async (args) => {
29
32
  try {
30
- const data = await getPoolInfo(args.poolId);
33
+ const chainId = args.chainId ?? getChainId();
34
+ const data = await getPoolInfo(args.poolId, chainId);
31
35
  return { content: [{ type: "text", text: JSON.stringify({ status: "success", data }, (_, v) => typeof v === 'bigint' ? v.toString() : v, 2) }] };
32
36
  }
33
37
  catch (error) {
@@ -41,11 +45,13 @@ export const getLiquidityInfoTool = {
41
45
  schema: {
42
46
  poolId: z.string().describe("Pool ID"),
43
47
  marketPrice: z.string().describe("Current market price (can be human-readable e.g. '0.69' or 30-decimal raw units)"),
48
+ chainId: z.coerce.number().optional().describe("Optional chainId override"),
44
49
  },
45
50
  handler: async (args) => {
46
51
  try {
47
52
  const { client } = await resolveClient();
48
- const data = await getLiquidityInfo(client, args.poolId, args.marketPrice);
53
+ const chainId = args.chainId ?? getChainId();
54
+ const data = await getLiquidityInfo(client, args.poolId, args.marketPrice, chainId);
49
55
  return { content: [{ type: "text", text: JSON.stringify({ status: "success", data }, (_, v) => typeof v === 'bigint' ? v.toString() : v, 2) }] };
50
56
  }
51
57
  catch (error) {
@@ -27,8 +27,8 @@ export const getOrderHistoryTool = {
27
27
  description: "Get historical orders with optional pool filter and pagination.",
28
28
  schema: {
29
29
  poolId: z.string().optional().describe("Filter by pool ID"),
30
- page: z.number().optional().describe("Page number (default 1)"),
31
- limit: z.number().optional().describe("Results per page (default 20)"),
30
+ page: z.coerce.number().optional().describe("Page number (default 1)"),
31
+ limit: z.coerce.number().optional().describe("Results per page (default 20)"),
32
32
  },
33
33
  handler: async (args) => {
34
34
  try {
@@ -1,16 +1,18 @@
1
1
  import { z } from "zod";
2
- import { resolveClient } from "../auth/resolveClient.js";
2
+ import { resolveClient, getChainId } from "../auth/resolveClient.js";
3
3
  import { getPoolLevelConfig } from "../services/marketService.js";
4
4
  export const getPoolLevelConfigTool = {
5
5
  name: "get_pool_level_config",
6
6
  description: "Get the level configuration and trading limits for a specific pool.",
7
7
  schema: {
8
8
  poolId: z.string().describe("Pool ID"),
9
+ chainId: z.coerce.number().optional().describe("Optional chainId override"),
9
10
  },
10
11
  handler: async (args) => {
11
12
  try {
12
13
  const { client } = await resolveClient();
13
- const result = await getPoolLevelConfig(client, args.poolId);
14
+ const chainId = args.chainId ?? getChainId();
15
+ const result = await getPoolLevelConfig(client, args.poolId, chainId);
14
16
  return { content: [{ type: "text", text: JSON.stringify({ status: "success", data: result }, (_, v) => typeof v === 'bigint' ? v.toString() : v, 2) }] };
15
17
  }
16
18
  catch (error) {
@@ -6,8 +6,8 @@ export const getPositionHistoryTool = {
6
6
  description: "Get historical closed positions with optional pool filter and pagination.",
7
7
  schema: {
8
8
  poolId: z.string().optional().describe("Filter by pool ID"),
9
- page: z.number().optional().describe("Page number (default 1)"),
10
- limit: z.number().optional().describe("Results per page (default 20)"),
9
+ page: z.coerce.number().optional().describe("Page number (default 1)"),
10
+ limit: z.coerce.number().optional().describe("Results per page (default 20)"),
11
11
  },
12
12
  handler: async (args) => {
13
13
  try {
@@ -6,7 +6,7 @@ export const searchMarketTool = {
6
6
  description: "Search for an active market by keyword.",
7
7
  schema: {
8
8
  keyword: z.string().describe('Search keyword, e.g. "BTC", "ETH"'),
9
- limit: z.number().optional().describe("Max results (default 100)"),
9
+ limit: z.coerce.number().optional().describe("Max results (default 100)"),
10
10
  },
11
11
  handler: async (args) => {
12
12
  try {
@@ -1,18 +1,22 @@
1
1
  import { z } from "zod";
2
2
  import { resolveClient } from "../auth/resolveClient.js";
3
3
  import { setPositionTpSl } from "../services/tradeService.js";
4
+ import { bpsLikeString } from "../utils/schema.js";
4
5
  export const setTpSlTool = {
5
6
  name: "set_tp_sl",
6
7
  description: "Set take profit and stop loss prices for an open position.",
7
8
  schema: {
8
9
  poolId: z.string().describe("Pool ID"),
9
10
  positionId: z.string().describe("Position ID"),
10
- direction: z.union([z.literal(0), z.literal(1)]).describe("0 = LONG, 1 = SHORT (position direction)"),
11
- leverage: z.number().describe("Position leverage"),
12
- tpPrice: z.string().optional().describe("Take-profit trigger price (human-readable)"),
13
- tpSize: z.string().optional().describe("TP size (18 decimals). Omit or '0' for full position."),
14
- slPrice: z.string().optional().describe("Stop-loss trigger price (human-readable)"),
15
- slSize: z.string().optional().describe("SL size (18 decimals). Omit or '0' for full position."),
11
+ direction: z.coerce.number().int().refine((value) => value === 0 || value === 1, {
12
+ message: "direction must be 0 (LONG) or 1 (SHORT)",
13
+ }).describe("0 = LONG, 1 = SHORT (position direction)"),
14
+ leverage: z.coerce.number().describe("Position leverage"),
15
+ tpPrice: z.string().optional().describe("SDK standard: 30-decimal raw TP price. Compatibility: human-readable decimals are also accepted."),
16
+ tpSize: z.string().optional().describe("SDK standard: 18-decimal raw TP size. Omit or '0' for full position."),
17
+ slPrice: z.string().optional().describe("SDK standard: 30-decimal raw SL price. Compatibility: human-readable decimals are also accepted."),
18
+ slSize: z.string().optional().describe("SDK standard: 18-decimal raw SL size. Omit or '0' for full position."),
19
+ slippagePct: bpsLikeString.optional().describe("Slippage in bps (4-digit precision): 1 = 0.01%, default 100 = 1%"),
16
20
  quoteToken: z.string().optional().describe("Quote token address"),
17
21
  },
18
22
  handler: async (args) => {
@@ -1,17 +1,21 @@
1
1
  import { z } from "zod";
2
2
  import { resolveClient } from "../auth/resolveClient.js";
3
3
  import { updateOrderTpSl } from "../services/tradeService.js";
4
+ import { booleanLike } from "../utils/schema.js";
4
5
  export const updateOrderTpSlTool = {
5
6
  name: "update_order_tp_sl",
6
7
  description: "Update an existing take profit or stop loss order.",
7
8
  schema: {
8
9
  orderId: z.string().describe("The ID of the order to update"),
9
10
  marketId: z.string().describe("The market ID (config hash) for the order"),
10
- tpPrice: z.string().optional().describe("New take-profit trigger price (human-readable)"),
11
- tpSize: z.string().optional().describe("New TP size (18 decimals). Omit or '0' for full position."),
12
- slPrice: z.string().optional().describe("New stop-loss trigger price (human-readable)"),
13
- slSize: z.string().optional().describe("New SL size (18 decimals). Omit or '0' for full position."),
14
- isTpSlOrder: z.boolean().optional().describe("Whether this is a TP/SL order (default true)"),
11
+ size: z.string().optional().describe("SDK standard: 18-decimal raw order size."),
12
+ price: z.string().optional().describe("SDK standard: 30-decimal raw order price."),
13
+ tpPrice: z.string().optional().describe("SDK standard: 30-decimal raw TP price. Compatibility: human-readable decimals are also accepted."),
14
+ tpSize: z.string().optional().describe("SDK standard: 18-decimal raw TP size. Omit or '0' for full position."),
15
+ slPrice: z.string().optional().describe("SDK standard: 30-decimal raw SL price. Compatibility: human-readable decimals are also accepted."),
16
+ slSize: z.string().optional().describe("SDK standard: 18-decimal raw SL size. Omit or '0' for full position."),
17
+ useOrderCollateral: booleanLike.optional().describe("Whether to use the order collateral (SDK default true)."),
18
+ isTpSlOrder: booleanLike.optional().describe("Whether this is a TP/SL order (default true)"),
15
19
  quoteToken: z.string().optional().describe("Quote token address"),
16
20
  },
17
21
  handler: async (args) => {
@@ -0,0 +1,84 @@
1
+ import { z } from "zod";
2
+ const trueValues = new Set(["true", "1", "yes", "y", "on"]);
3
+ const falseValues = new Set(["false", "0", "no", "n", "off"]);
4
+ const DECIMAL_RE = /^-?\d+(\.\d+)?$/;
5
+ const EPSILON = 1e-8;
6
+ /**
7
+ * Safe boolean coercion for MCP inputs:
8
+ * - boolean: true/false
9
+ * - number: 1/0
10
+ * - string: true/false/1/0/yes/no/on/off (case-insensitive)
11
+ */
12
+ export const booleanLike = z.union([
13
+ z.boolean(),
14
+ z.number().refine((value) => value === 0 || value === 1, {
15
+ message: "Expected 0 or 1 for boolean value.",
16
+ }).transform((value) => value === 1),
17
+ z.string().trim().toLowerCase().refine((value) => trueValues.has(value) || falseValues.has(value), {
18
+ message: "Expected boolean-like value: true/false/1/0/yes/no/on/off.",
19
+ }).transform((value) => trueValues.has(value)),
20
+ ]);
21
+ function isNearlyInteger(value) {
22
+ return Math.abs(value - Math.round(value)) < EPSILON;
23
+ }
24
+ function parseBpsLike(value) {
25
+ const raw = String(value).trim();
26
+ if (!raw)
27
+ throw new Error("slippage is required.");
28
+ if (!DECIMAL_RE.test(raw))
29
+ throw new Error("slippage must be numeric.");
30
+ const parsed = Number(raw);
31
+ if (!Number.isFinite(parsed))
32
+ throw new Error("slippage must be finite.");
33
+ if (parsed < 0)
34
+ throw new Error("slippage must be >= 0.");
35
+ if (isNearlyInteger(parsed)) {
36
+ return Math.round(parsed);
37
+ }
38
+ // Backward compatibility: legacy ratio input (e.g. 0.01 = 1%) -> bps
39
+ if (parsed > 0 && parsed < 1) {
40
+ const bps = parsed * 10000;
41
+ if (!isNearlyInteger(bps)) {
42
+ throw new Error("legacy slippage ratio is too precise; use integer bps.");
43
+ }
44
+ return Math.round(bps);
45
+ }
46
+ throw new Error("slippage must be integer bps (1 = 0.01%).");
47
+ }
48
+ function parseLpSlippageLike(value) {
49
+ const raw = String(value).trim();
50
+ if (!raw)
51
+ throw new Error("slippage is required.");
52
+ if (!DECIMAL_RE.test(raw))
53
+ throw new Error("slippage must be numeric.");
54
+ const parsed = Number(raw);
55
+ if (!Number.isFinite(parsed))
56
+ throw new Error("slippage must be finite.");
57
+ if (parsed < 0)
58
+ throw new Error("slippage must be >= 0.");
59
+ // SDK doc convention for LP: ratio (e.g. 0.01 = 1%)
60
+ if (parsed <= 1) {
61
+ return parsed;
62
+ }
63
+ // Backward compatibility for MCP callers that send BPS (e.g. 100 = 1%)
64
+ return parsed / 10000;
65
+ }
66
+ export const bpsLikeNumber = z.union([z.number(), z.string()]).transform((value, ctx) => {
67
+ try {
68
+ return parseBpsLike(value);
69
+ }
70
+ catch (error) {
71
+ ctx.addIssue({ code: "custom", message: error.message || "Invalid slippage." });
72
+ return z.NEVER;
73
+ }
74
+ });
75
+ export const bpsLikeString = bpsLikeNumber.transform((value) => value.toString());
76
+ export const lpSlippageLike = z.union([z.number(), z.string()]).transform((value, ctx) => {
77
+ try {
78
+ return parseLpSlippageLike(value);
79
+ }
80
+ catch (error) {
81
+ ctx.addIssue({ code: "custom", message: error.message || "Invalid LP slippage." });
82
+ return z.NEVER;
83
+ }
84
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@michaleffffff/mcp-trading-server",
3
- "version": "2.3.5",
3
+ "version": "2.4.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "myx-mcp": "dist/server.js"