@michaleffffff/mcp-trading-server 2.4.0 → 2.4.1

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.4.0" }, { capabilities: { tools: {}, resources: {}, prompts: {} } });
84
+ const server = new Server({ name: "myx-mcp-trading-server", version: "2.4.1" }, { capabilities: { tools: {}, resources: {}, prompts: {} } });
85
85
  // List tools
86
86
  server.setRequestHandler(ListToolsRequestSchema, async () => {
87
87
  return {
@@ -105,7 +105,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
105
105
  try {
106
106
  let validatedArgs = args ?? {};
107
107
  if (tool.schema) {
108
- validatedArgs = z.object(tool.schema).parse(args ?? {});
108
+ validatedArgs = z.object(tool.schema).strict().parse(args ?? {});
109
109
  }
110
110
  logger.toolExecution(name, validatedArgs);
111
111
  const result = await tool.handler(validatedArgs);
@@ -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.4.0 running (stdio, pure on-chain, prod ready)");
184
+ logger.info("🚀 MYX Trading MCP Server v2.4.1 running (stdio, pure on-chain, prod ready)");
185
185
  }
186
186
  main().catch((err) => {
187
187
  logger.error("Fatal Server Startup Error", err);
@@ -14,7 +14,7 @@ export async function getOraclePrice(client, poolId, chainIdOverride) {
14
14
  }
15
15
  export async function searchMarket(client, keyword, limit = 1000) {
16
16
  const chainId = getChainId();
17
- const searchRes = await client.markets.searchMarket({ chainId, searchKey: keyword, limit });
17
+ const searchRes = await client.markets.searchMarket({ chainId, keyword, limit });
18
18
  const raw = searchRes;
19
19
  let dataList = [];
20
20
  if (raw && raw.contractInfo && Array.isArray(raw.contractInfo.list)) {
@@ -44,7 +44,7 @@ export async function searchMarket(client, keyword, limit = 1000) {
44
44
  }
45
45
  }
46
46
  return activeMarkets.map(m => {
47
- const ticker = tickers.find(t => t.poolId.toLowerCase() === m.poolId.toLowerCase());
47
+ const ticker = tickers.find(t => String(t.poolId).toLowerCase() === String(m.poolId).toLowerCase());
48
48
  return {
49
49
  symbol: m.baseQuoteSymbol || `${m.baseSymbol}/${m.quoteSymbol}`,
50
50
  name: m.symbolName || m.name,
@@ -76,5 +76,5 @@ export async function getPoolList(client) {
76
76
  */
77
77
  export async function getPoolLevelConfig(client, poolId, chainIdOverride) {
78
78
  const chainId = chainIdOverride ?? getChainId();
79
- return client.api.getPoolLevelConfig({ poolId, chainId });
79
+ return client.markets.getPoolLevelConfig(poolId, chainId);
80
80
  }
@@ -1,6 +1,5 @@
1
1
  import { pool, quote, base } from "@myx-trade/sdk";
2
2
  import { getChainId, resolveClient } from "../auth/resolveClient.js";
3
- import { ensureUnits } from "../utils/units.js";
4
3
  /**
5
4
  * 创建合约市场池子
6
5
  */
@@ -28,8 +27,7 @@ export async function getPoolDetail(poolId, chainIdOverride) {
28
27
  */
29
28
  export async function getLiquidityInfo(client, poolId, marketPrice, chainIdOverride) {
30
29
  const chainId = chainIdOverride ?? getChainId();
31
- const price30 = ensureUnits(marketPrice, 30, "marketPrice");
32
- return client.utils.getLiquidityInfo({ chainId, poolId, marketPrice: price30 });
30
+ return client.utils.getLiquidityInfo({ chainId, poolId, marketPrice });
33
31
  }
34
32
  /**
35
33
  * Quote 池 deposit
@@ -1,6 +1,6 @@
1
1
  import { Direction, OrderType, TriggerType } from "@myx-trade/sdk";
2
2
  import { parseUnits } from "ethers";
3
- import { getChainId, getQuoteToken, getQuoteDecimals } from "../auth/resolveClient.js";
3
+ import { getChainId, getQuoteToken } from "../auth/resolveClient.js";
4
4
  import { ensureUnits } from "../utils/units.js";
5
5
  import { normalizeAddress } from "../utils/address.js";
6
6
  /**
@@ -20,115 +20,61 @@ function resolveDirection(direction) {
20
20
  * 开仓 / 加仓
21
21
  */
22
22
  export async function openPosition(client, address, args) {
23
- const maxTradeAmount = process.env.MAX_TRADE_AMOUNT;
24
- if (maxTradeAmount && Number(args.collateralAmount) > Number(maxTradeAmount)) {
25
- throw new Error(`Security Exception: collateralAmount (${args.collateralAmount}) exceeds MAX_TRADE_AMOUNT (${maxTradeAmount}). Update .env if you need to trade larger sizes.`);
26
- }
27
23
  const chainId = getChainId();
28
- const quoteToken = normalizeAddress(args.quoteToken || getQuoteToken(), "quoteToken");
29
- const quoteDecimals = args.quoteDecimals ?? getQuoteDecimals();
30
- const rawLeverage = args.leverage ?? 10;
31
- if (!Number.isFinite(rawLeverage) || rawLeverage <= 0) {
32
- throw new Error("leverage must be a positive number.");
33
- }
34
- const leverage = Math.floor(rawLeverage);
35
- let calcPrice30;
36
- let orderPrice30;
37
- if (args.price) {
38
- const limitPrice30 = ensureUnits(args.price, 30, "price");
39
- calcPrice30 = limitPrice30;
40
- orderPrice30 = limitPrice30;
41
- }
42
- else {
43
- const oracleData = await client.utils.getOraclePrice(args.poolId, chainId);
44
- calcPrice30 = ensureUnits(oracleData.price, 30, "oracle price");
45
- orderPrice30 = calcPrice30;
46
- console.error(`[Market Order] Fetched oracle price for size math: ${calcPrice30}`);
47
- }
48
- // 保证金转为 quote token 精度
49
- const collateralWei = ensureUnits(args.collateralAmount, quoteDecimals, "collateralAmount");
50
- // 仓位大小 (size): 在 MYX 这一版中,size 表示 Base Token(WETH)的数量,精度通常为 18
51
- // 计算公式:size = (collateral * leverage) / price
52
- // 为了防止浮点溢出,我们可以全部转为 BigInt 计算
53
- const collateralBig = BigInt(collateralWei);
54
- const leverageBig = BigInt(leverage);
55
- const priceBig = BigInt(calcPrice30);
56
- let size = "0";
57
- if (priceBig > 0n) {
58
- // collateral: 6位, price: 30位, 目标size: 18位
59
- // (collateral * leverage * 10^(18 + 30 - 6)) / price
60
- const exponent = BigInt(18 + 30 - quoteDecimals);
61
- size = ((collateralBig * leverageBig * (10n ** exponent)) / priceBig).toString();
62
- }
63
24
  const dir = resolveDirection(args.direction);
64
- // 根据方向决定 triggerType: 做多分级(如果现价>限价则是低买,需等跌,用 LTE;反之亦然,这里简化为通用 LTE(多)/GTE(空))
65
- const triggerType = args.price
66
- ? (args.direction === 0 ? TriggerType.LTE : TriggerType.GTE)
67
- : TriggerType.NONE; // 没传价就是市价单
25
+ const executionFeeToken = normalizeAddress(args.executionFeeToken, "executionFeeToken");
68
26
  const orderParams = {
69
27
  chainId,
70
28
  address,
71
29
  poolId: args.poolId,
72
- // 对于新开仓,传空字符串 ""
73
- positionId: args.positionId || "",
74
- orderType: args.price ? OrderType.LIMIT : OrderType.MARKET,
75
- triggerType: triggerType,
30
+ positionId: args.positionId,
31
+ orderType: args.orderType,
32
+ triggerType: args.triggerType,
76
33
  direction: dir,
77
- collateralAmount: collateralWei,
78
- size,
79
- price: orderPrice30,
80
- timeInForce: 0,
81
- postOnly: false,
82
- slippagePct: args.slippagePct ?? "100", // BPS
83
- executionFeeToken: quoteToken,
84
- leverage,
34
+ collateralAmount: args.collateralAmount,
35
+ size: args.size,
36
+ price: args.price,
37
+ timeInForce: args.timeInForce,
38
+ postOnly: args.postOnly,
39
+ slippagePct: args.slippagePct,
40
+ executionFeeToken,
41
+ leverage: args.leverage,
85
42
  };
86
- // 可选止盈止损
87
- if (args.tpPrice) {
88
- orderParams.tpPrice = ensureUnits(args.tpPrice, 30, "tpPrice");
89
- orderParams.tpSize = size;
90
- }
91
- if (args.slPrice) {
92
- orderParams.slPrice = ensureUnits(args.slPrice, 30, "slPrice");
93
- orderParams.slSize = size;
94
- }
95
- // tradingFee: SDK 第二个参数
96
- const tradingFeeStr = (args.tradingFee || "0").toString();
97
- // 获取 marketId,SDK 0.1.267 要求 as 3rd parameter
98
- const marketDetailRes = await client.markets.getMarketDetail({ chainId, poolId: args.poolId });
99
- const marketId = marketDetailRes?.marketId || marketDetailRes?.data?.marketId;
100
- if (!marketId) {
101
- throw new Error(`Could not find marketId for poolId: ${args.poolId}`);
102
- }
103
- return client.order.createIncreaseOrder(orderParams, tradingFeeStr, marketId);
43
+ if (args.tpSize)
44
+ orderParams.tpSize = args.tpSize;
45
+ if (args.tpPrice)
46
+ orderParams.tpPrice = args.tpPrice;
47
+ if (args.slSize)
48
+ orderParams.slSize = args.slSize;
49
+ if (args.slPrice)
50
+ orderParams.slPrice = args.slPrice;
51
+ return client.order.createIncreaseOrder(orderParams, args.tradingFee, args.marketId);
104
52
  }
105
53
  /**
106
54
  * 平仓 / 减仓
107
55
  */
108
56
  export async function closePosition(client, address, args) {
109
57
  const chainId = getChainId();
110
- const quoteToken = normalizeAddress(args.quoteToken || getQuoteToken(), "quoteToken");
58
+ const executionFeeToken = normalizeAddress(args.executionFeeToken, "executionFeeToken");
111
59
  if (args.direction === undefined || args.direction === null) {
112
60
  throw new Error("direction is required (0=LONG, 1=SHORT), must match position direction.");
113
61
  }
114
- const price30 = ensureUnits(args.price, 30, "price");
115
- const sizeWei = ensureUnits(args.size, 18, "size");
116
62
  const dir = resolveDirection(args.direction);
117
63
  return client.order.createDecreaseOrder({
118
64
  chainId,
119
65
  address,
120
66
  poolId: args.poolId,
121
67
  positionId: args.positionId,
122
- orderType: OrderType.MARKET,
123
- triggerType: TriggerType.NONE,
68
+ orderType: args.orderType,
69
+ triggerType: args.triggerType,
124
70
  direction: dir,
125
- collateralAmount: "0",
126
- size: sizeWei,
127
- price: price30,
128
- timeInForce: 0,
129
- postOnly: false,
130
- slippagePct: args.slippagePct ?? "100",
131
- executionFeeToken: quoteToken,
71
+ collateralAmount: args.collateralAmount,
72
+ size: args.size,
73
+ price: args.price,
74
+ timeInForce: args.timeInForce,
75
+ postOnly: args.postOnly,
76
+ slippagePct: args.slippagePct,
77
+ executionFeeToken,
132
78
  leverage: args.leverage,
133
79
  });
134
80
  }
@@ -137,7 +83,7 @@ export async function closePosition(client, address, args) {
137
83
  */
138
84
  export async function setPositionTpSl(client, address, args) {
139
85
  const chainId = getChainId();
140
- const quoteToken = normalizeAddress(args.quoteToken || getQuoteToken(), "quoteToken");
86
+ const executionFeeToken = normalizeAddress(args.executionFeeToken, "executionFeeToken");
141
87
  if (!args.tpPrice && !args.slPrice) {
142
88
  throw new Error("At least one of tpPrice or slPrice must be provided.");
143
89
  }
@@ -149,19 +95,19 @@ export async function setPositionTpSl(client, address, args) {
149
95
  positionId: args.positionId,
150
96
  direction: dir,
151
97
  leverage: args.leverage,
152
- executionFeeToken: quoteToken,
153
- tpTriggerType: args.direction === 0 ? TriggerType.GTE : TriggerType.LTE,
154
- slTriggerType: args.direction === 0 ? TriggerType.LTE : TriggerType.GTE,
155
- slippagePct: args.slippagePct ?? "100",
98
+ executionFeeToken,
99
+ tpTriggerType: args.tpTriggerType,
100
+ slTriggerType: args.slTriggerType,
101
+ slippagePct: args.slippagePct,
156
102
  };
157
- if (args.tpPrice) {
158
- params.tpPrice = ensureUnits(args.tpPrice, 30, "tpPrice");
159
- params.tpSize = args.tpSize || "0";
160
- }
161
- if (args.slPrice) {
162
- params.slPrice = ensureUnits(args.slPrice, 30, "slPrice");
163
- params.slSize = args.slSize || "0";
164
- }
103
+ if (args.tpPrice)
104
+ params.tpPrice = args.tpPrice;
105
+ if (args.tpSize)
106
+ params.tpSize = args.tpSize;
107
+ if (args.slPrice)
108
+ params.slPrice = args.slPrice;
109
+ if (args.slSize)
110
+ params.slSize = args.slSize;
165
111
  return client.order.createPositionTpSlOrder(params);
166
112
  }
167
113
  /**
@@ -170,11 +116,14 @@ export async function setPositionTpSl(client, address, args) {
170
116
  export async function adjustMargin(client, address, args) {
171
117
  const chainId = getChainId();
172
118
  const quoteToken = normalizeAddress(args.quoteToken || getQuoteToken(), "quoteToken");
173
- const quoteDecimals = args.quoteDecimals ?? getQuoteDecimals();
119
+ const adjustAmount = String(args.adjustAmount ?? "").trim();
120
+ if (!/^-?\d+$/.test(adjustAmount)) {
121
+ throw new Error("adjustAmount must be an integer string in quote token raw units.");
122
+ }
174
123
  return client.position.adjustCollateral({
175
124
  poolId: args.poolId,
176
125
  positionId: args.positionId,
177
- adjustAmount: ensureUnits(args.adjustAmount, quoteDecimals, "adjustAmount"),
126
+ adjustAmount,
178
127
  quoteToken,
179
128
  chainId,
180
129
  address,
@@ -232,17 +181,17 @@ export async function closeAllPositions(client, address) {
232
181
  */
233
182
  export async function updateOrderTpSl(client, address, args) {
234
183
  const chainId = getChainId();
235
- const quoteToken = normalizeAddress(args.quoteToken || getQuoteToken(), "quoteToken");
184
+ const quoteToken = normalizeAddress(args.quoteToken, "quoteToken");
236
185
  const params = {
237
186
  orderId: args.orderId,
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",
242
- useOrderCollateral: args.useOrderCollateral ?? true,
187
+ tpSize: args.tpSize,
188
+ tpPrice: args.tpPrice,
189
+ slSize: args.slSize,
190
+ slPrice: args.slPrice,
191
+ useOrderCollateral: args.useOrderCollateral,
243
192
  executionFeeToken: quoteToken,
244
- size: args.size ? ensureUnits(args.size, 18, "size") : "0",
245
- price: args.price ? ensureUnits(args.price, 30, "price") : "0",
193
+ size: args.size,
194
+ price: args.price,
246
195
  };
247
- return client.order.updateOrderTpSl(params, quoteToken, chainId, address, args.marketId, args.isTpSlOrder ?? true);
196
+ return client.order.updateOrderTpSl(params, quoteToken, chainId, address, args.marketId, args.isTpSlOrder);
248
197
  }
@@ -41,14 +41,13 @@ export const getTradeFlowTool = {
41
41
  description: "Get account trade flow / transaction history.",
42
42
  schema: {
43
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)"),
44
+ limit: z.number().int().positive().optional().describe("Results per page (default 20)"),
46
45
  },
47
46
  handler: async (args) => {
48
47
  try {
49
48
  const { client, address } = await resolveClient();
50
49
  const chainId = getChainId();
51
- const query = { chainId, poolId: args.poolId, page: args.page ?? 1, limit: args.limit ?? 20 };
50
+ const query = { chainId, poolId: args.poolId, limit: args.limit ?? 20 };
52
51
  const result = await client.account.getTradeFlow(query, address);
53
52
  const enhancedData = (result?.data || []).map((flow) => ({
54
53
  ...flow,
@@ -1,19 +1,18 @@
1
1
  import { z } from "zod";
2
- import { resolveClient, getChainId, getQuoteToken } from "../auth/resolveClient.js";
2
+ import { resolveClient, getChainId } from "../auth/resolveClient.js";
3
3
  import { normalizeAddress } from "../utils/address.js";
4
- import { booleanLike } from "../utils/schema.js";
5
4
  export const accountDepositTool = {
6
5
  name: "account_deposit",
7
6
  description: "Deposit funds from wallet into the MYX trading account.",
8
7
  schema: {
9
- amount: z.string().describe("Amount to deposit (in token decimals)"),
10
- tokenAddress: z.string().optional().describe("Token address (default: quote token)"),
8
+ amount: z.string().regex(/^\d+$/).describe("Amount to deposit (token raw units)"),
9
+ tokenAddress: z.string().describe("Token address"),
11
10
  },
12
11
  handler: async (args) => {
13
12
  try {
14
13
  const { client } = await resolveClient();
15
14
  const chainId = getChainId();
16
- const tokenAddress = normalizeAddress(args.tokenAddress || getQuoteToken(), "tokenAddress");
15
+ const tokenAddress = normalizeAddress(args.tokenAddress, "tokenAddress");
17
16
  const result = await client.account.deposit({
18
17
  amount: args.amount,
19
18
  tokenAddress,
@@ -31,8 +30,8 @@ export const accountWithdrawTool = {
31
30
  description: "Withdraw funds from MYX trading account back to wallet.",
32
31
  schema: {
33
32
  poolId: z.string().describe("Pool ID to withdraw from"),
34
- amount: z.string().describe("Amount to withdraw (in token decimals)"),
35
- isQuoteToken: booleanLike.optional().describe("Whether to withdraw as quote token (default true)"),
33
+ amount: z.string().regex(/^\d+$/).describe("Amount to withdraw (token raw units)"),
34
+ isQuoteToken: z.boolean().describe("Whether to withdraw as quote token"),
36
35
  },
37
36
  handler: async (args) => {
38
37
  try {
@@ -43,7 +42,7 @@ export const accountWithdrawTool = {
43
42
  receiver: address,
44
43
  amount: args.amount,
45
44
  poolId: args.poolId,
46
- isQuoteToken: args.isQuoteToken !== false,
45
+ isQuoteToken: args.isQuoteToken,
47
46
  });
48
47
  return { content: [{ type: "text", text: JSON.stringify({ status: "success", data: result }, (_, v) => typeof v === 'bigint' ? v.toString() : v, 2) }] };
49
48
  }
@@ -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("SDK standard: quote token raw units. Positive = add, negative = remove. Compatibility: human-readable decimals are also accepted."),
10
+ adjustAmount: z.string().regex(/^-?\d+$/).describe("Quote token raw units. Positive = add, negative = remove."),
11
11
  quoteToken: z.string().optional().describe("Quote token address"),
12
12
  },
13
13
  handler: async (args) => {
@@ -2,28 +2,15 @@ import { z } from "zod";
2
2
  import { resolveClient, getChainId } from "../auth/resolveClient.js";
3
3
  export const cancelAllOrdersTool = {
4
4
  name: "cancel_all_orders",
5
- description: "Cancel all open orders, or cancel the provided orderIds in one call.",
5
+ description: "Cancel multiple open orders by orderIds.",
6
6
  schema: {
7
- orderIds: z.array(z.string()).optional().describe("Optional explicit order IDs. If omitted, all open orders will be cancelled."),
7
+ orderIds: z.array(z.string()).min(1).describe("Order IDs to cancel."),
8
8
  },
9
9
  handler: async (args) => {
10
10
  try {
11
- const { client, address } = await resolveClient();
11
+ const { client } = await resolveClient();
12
12
  const chainId = getChainId();
13
- let orderIds = Array.isArray(args.orderIds) ? args.orderIds : [];
14
- if (orderIds.length === 0) {
15
- const openOrders = await client.order.getOrders(address);
16
- const rows = Array.isArray(openOrders?.data) ? openOrders.data : [];
17
- orderIds = rows
18
- .map((o) => o.orderId ?? o.id ?? o.order_id)
19
- .filter((id) => id !== undefined && id !== null)
20
- .map((id) => String(id));
21
- }
22
- if (orderIds.length === 0) {
23
- return {
24
- content: [{ type: "text", text: JSON.stringify({ status: "success", data: { message: "No open orders to cancel.", cancelled: 0 } }, (_, v) => typeof v === "bigint" ? v.toString() : v, 2) }],
25
- };
26
- }
13
+ const orderIds = args.orderIds;
27
14
  const result = await client.order.cancelAllOrders(orderIds, chainId);
28
15
  return {
29
16
  content: [{ type: "text", text: JSON.stringify({ status: "success", data: { cancelled: orderIds.length, orderIds, result } }, (_, v) => typeof v === "bigint" ? v.toString() : v, 2) }],
@@ -4,23 +4,13 @@ export const cancelOrderTool = {
4
4
  name: "cancel_order",
5
5
  description: "Cancel an open order by its order ID.",
6
6
  schema: {
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"),
7
+ orderId: z.string().describe("Order ID to cancel"),
9
8
  },
10
9
  handler: async (args) => {
11
10
  try {
12
11
  const { client } = await resolveClient();
13
12
  const chainId = getChainId();
14
- const ids = args.orderId ? [args.orderId] : (Array.isArray(args.orderIds) ? args.orderIds : []);
15
- if (!ids || ids.length === 0)
16
- throw new Error("orderId or orderIds is required.");
17
- let result;
18
- if (ids.length === 1) {
19
- result = await client.order.cancelOrder(ids[0], chainId);
20
- }
21
- else {
22
- result = await client.order.cancelOrders(ids, chainId);
23
- }
13
+ const result = await client.order.cancelOrder(args.orderId, chainId);
24
14
  return { content: [{ type: "text", text: JSON.stringify({ status: "success", data: result }, (_, v) => typeof v === 'bigint' ? v.toString() : v, 2) }] };
25
15
  }
26
16
  catch (error) {
@@ -1,16 +1,15 @@
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";
5
4
  const MAX_UINT256 = "115792089237316195423570985008687907853269984665640564039457584007913129639935";
6
5
  export const checkApprovalTool = {
7
6
  name: "check_approval",
8
7
  description: "Check if token spending approval is needed. Supports auto-approve exact amount (default) or optional unlimited approval.",
9
8
  schema: {
10
- amount: z.string().describe("Amount to check approval for (in token decimals)"),
9
+ amount: z.string().regex(/^\d+$/).describe("Amount to check approval for (token raw units)"),
11
10
  quoteToken: z.string().optional().describe("Token address to check. Uses default if omitted."),
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)."),
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)."),
14
13
  },
15
14
  handler: async (args) => {
16
15
  try {
@@ -2,13 +2,12 @@ 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";
6
5
  export const closeAllPositionsTool = {
7
6
  name: "close_all_positions",
8
7
  description: "Emergency: close ALL open positions in a pool at once. Use for risk management.",
9
8
  schema: {
10
9
  poolId: z.string().describe("Pool ID to close all positions in"),
11
- slippagePct: bpsLikeString.optional().describe("Slippage in bps (4-digit precision): 1 = 0.01%, default 200 = 2% for emergency"),
10
+ slippagePct: z.string().regex(/^\d+$/).optional().describe("Slippage in 4-digit precision raw units (1 = 0.01%)"),
12
11
  },
13
12
  handler: async (args) => {
14
13
  try {
@@ -61,7 +60,7 @@ export const closeAllPositionsTool = {
61
60
  postOnly: false,
62
61
  slippagePct,
63
62
  executionFeeToken: pos.quoteToken || pos.quote_token || getQuoteToken(),
64
- leverage: pos.leverage || 1,
63
+ leverage: pos.userLeverage ?? pos.leverage ?? 1,
65
64
  };
66
65
  });
67
66
  const result = await client.order.closeAllPositions(chainId, closeParams);
@@ -1,21 +1,23 @@
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";
5
4
  export const closePositionTool = {
6
5
  name: "close_position",
7
- description: "Close an open position.",
6
+ description: "Create a decrease order using SDK-native parameters.",
8
7
  schema: {
9
8
  poolId: z.string().describe("Pool ID"),
10
9
  positionId: z.string().describe("Position ID to close"),
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%"),
18
- quoteToken: z.string().optional().describe("Quote token address"),
10
+ orderType: z.number().int().min(0).max(3).describe("OrderType enum value"),
11
+ triggerType: z.number().int().min(0).max(2).describe("TriggerType enum value"),
12
+ direction: z.union([z.literal(0), z.literal(1)]).describe("0 = LONG, 1 = SHORT"),
13
+ collateralAmount: z.string().regex(/^\d+$/).describe("Collateral raw units"),
14
+ size: z.string().regex(/^\d+$/).describe("Position size raw units"),
15
+ price: z.string().regex(/^\d+$/).describe("Price raw units (30 decimals)"),
16
+ timeInForce: z.number().int().describe("TimeInForce enum value"),
17
+ postOnly: z.boolean().describe("Post-only flag"),
18
+ slippagePct: z.string().regex(/^\d+$/).describe("Slippage with 4-dec precision raw units"),
19
+ executionFeeToken: z.string().describe("Execution fee token address"),
20
+ leverage: z.number().describe("Leverage"),
19
21
  },
20
22
  handler: async (args) => {
21
23
  try {
@@ -1,24 +1,29 @@
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";
5
4
  export const executeTradeTool = {
6
5
  name: "execute_trade",
7
- description: "Execute a new trade or add to an existing position.",
6
+ description: "Create an increase order using SDK-native parameters.",
8
7
  schema: {
9
- poolId: z.string().describe("Pool ID to trade in"),
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."),
16
- quoteToken: z.string().optional().describe("Quote token address. Uses env default if omitted."),
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%"),
19
- positionId: z.string().optional().describe("Existing position ID to add to. Default '0' = new position."),
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."),
8
+ poolId: z.string().describe("Pool ID"),
9
+ positionId: z.string().describe("Position ID ('0' for new position)"),
10
+ orderType: z.number().int().min(0).max(3).describe("OrderType enum value"),
11
+ triggerType: z.number().int().min(0).max(2).describe("TriggerType enum value"),
12
+ direction: z.union([z.literal(0), z.literal(1)]).describe("0 = LONG, 1 = SHORT"),
13
+ collateralAmount: z.string().regex(/^\d+$/).describe("Quote token raw units"),
14
+ size: z.string().regex(/^\d+$/).describe("Position size raw units"),
15
+ price: z.string().regex(/^\d+$/).describe("Price raw units (30 decimals)"),
16
+ timeInForce: z.number().int().describe("TimeInForce enum value"),
17
+ postOnly: z.boolean().describe("Post-only flag"),
18
+ slippagePct: z.string().regex(/^\d+$/).describe("Slippage with 4-dec precision raw units"),
19
+ executionFeeToken: z.string().describe("Execution fee token address"),
20
+ leverage: z.number().describe("Leverage"),
21
+ tpSize: z.string().regex(/^\d+$/).optional().describe("TP size raw units"),
22
+ tpPrice: z.string().regex(/^\d+$/).optional().describe("TP price raw units (30 decimals)"),
23
+ slSize: z.string().regex(/^\d+$/).optional().describe("SL size raw units"),
24
+ slPrice: z.string().regex(/^\d+$/).optional().describe("SL price raw units (30 decimals)"),
25
+ tradingFee: z.string().regex(/^\d+$/).describe("Trading fee raw units"),
26
+ marketId: z.string().describe("Market ID"),
22
27
  },
23
28
  handler: async (args) => {
24
29
  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.coerce.number().optional().describe("Optional chainId override"),
7
+ chainId: z.number().int().positive().optional().describe("Optional chainId override"),
8
8
  },
9
9
  handler: async (args) => {
10
10
  try {
@@ -1,71 +1,17 @@
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
- }
42
3
  export const getBaseDetailTool = {
43
4
  name: "get_base_detail",
44
- description: "Get base asset detail metrics. SDK doc uses baseAddress; MCP also accepts poolId for compatibility.",
5
+ description: "Get base token details.",
45
6
  schema: {
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"),
7
+ baseAddress: z.string().describe("Base token address"),
8
+ chainId: z.number().int().positive().optional().describe("Optional chainId override"),
49
9
  },
50
10
  handler: async (args) => {
51
11
  try {
52
12
  const { client } = await resolveClient();
53
13
  const chainId = args.chainId ?? getChainId();
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 });
14
+ const result = await client.markets.getBaseDetail({ chainId, baseAddress: args.baseAddress });
69
15
  return { content: [{ type: "text", text: JSON.stringify({ status: "success", data: result }, (_, v) => typeof v === "bigint" ? v.toString() : v, 2) }] };
70
16
  }
71
17
  catch (error) {
@@ -1,12 +1,13 @@
1
1
  import { z } from "zod";
2
2
  import { resolveClient, getChainId } from "../auth/resolveClient.js";
3
+ const klineIntervalSchema = z.enum(["1m", "5m", "15m", "30m", "1h", "4h", "1d", "1w", "1M"]);
3
4
  export const getKlineTool = {
4
5
  name: "get_kline",
5
6
  description: "Get K-line / candlestick data for a pool. Essential for price analysis.",
6
7
  schema: {
7
8
  poolId: z.string().describe("Pool ID"),
8
- interval: z.string().describe("K-line interval: '1m','5m','15m','30m','1h','4h','1d','1w','1M'"),
9
- limit: z.coerce.number().optional().describe("Number of bars (default 100)"),
9
+ interval: klineIntervalSchema.describe("K-line interval"),
10
+ limit: z.number().int().positive().optional().describe("Number of bars (default 100)"),
10
11
  },
11
12
  handler: async (args) => {
12
13
  try {
@@ -1,11 +1,12 @@
1
1
  import { z } from "zod";
2
2
  import { resolveClient, getChainId } from "../auth/resolveClient.js";
3
+ const klineIntervalSchema = z.enum(["1m", "5m", "15m", "30m", "1h", "4h", "1d", "1w", "1M"]);
3
4
  export const getKlineLatestBarTool = {
4
5
  name: "get_kline_latest_bar",
5
6
  description: "Get the latest single K-line/candlestick bar for a pool.",
6
7
  schema: {
7
8
  poolId: z.string().describe("Pool ID"),
8
- interval: z.string().describe("K-line interval: '1m','5m','15m','30m','1h','4h','1d','1w','1M'"),
9
+ interval: klineIntervalSchema.describe("K-line interval"),
9
10
  },
10
11
  handler: async (args) => {
11
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.coerce.number().optional().describe("Max results to request (default 1000)."),
8
+ limit: z.number().int().positive().optional().describe("Max results to request (default 1000)."),
9
9
  },
10
10
  handler: async (args) => {
11
11
  try {
@@ -6,7 +6,7 @@ export const getMarketPriceTool = {
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
+ chainId: z.number().int().positive().optional().describe("Optional chainId override"),
10
10
  },
11
11
  handler: async (args) => {
12
12
  try {
@@ -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.coerce.number().optional().describe("Optional chainId override"),
8
+ chainId: z.number().int().positive().optional().describe("Optional chainId override"),
9
9
  },
10
10
  handler: async (args) => {
11
11
  try {
@@ -6,7 +6,7 @@ export const getOraclePriceTool = {
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
+ chainId: z.number().int().positive().optional().describe("Optional chainId override"),
10
10
  },
11
11
  handler: async (args) => {
12
12
  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.coerce.number().describe("Asset class ID"),
8
- riskTier: z.coerce.number().describe("Risk tier"),
9
- chainId: z.coerce.number().optional().describe("Optional chainId override"),
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"),
10
10
  },
11
11
  handler: async (args) => {
12
12
  try {
@@ -1,31 +1,20 @@
1
1
  import { z } from "zod";
2
2
  import { quoteDeposit, quoteWithdraw, baseDeposit, baseWithdraw, getLpPrice, } from "../services/poolService.js";
3
- import { parseSafeNumber } from "../utils/units.js";
4
- import { lpSlippageLike } from "../utils/schema.js";
5
3
  export const manageLiquidityTool = {
6
4
  name: "manage_liquidity",
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.",
5
+ description: "Add or withdraw liquidity from a BASE or QUOTE pool.",
8
6
  schema: {
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'"),
7
+ action: z.enum(["deposit", "withdraw"]).describe("'deposit' or 'withdraw'"),
8
+ poolType: z.enum(["BASE", "QUOTE"]).describe("'BASE' or 'QUOTE'"),
15
9
  poolId: z.string().describe("Pool ID"),
16
- amount: z.union([z.number(), z.string()]).describe("Amount in human-readable units (e.g. 2000 for USDC, 0.01 for ETH)"),
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"),
10
+ amount: z.number().positive().describe("Amount in human-readable units"),
11
+ slippage: z.number().min(0).describe("LP slippage ratio (e.g. 0.01 = 1%)"),
12
+ chainId: z.number().int().positive().optional().describe("Optional chainId override"),
19
13
  },
20
14
  handler: async (args) => {
21
15
  try {
22
16
  const { action, poolType, poolId } = args;
23
- const amount = parseSafeNumber(args.amount, "amount");
24
- const slippage = args.slippage;
25
- if (amount <= 0)
26
- throw new Error("amount must be a positive number.");
27
- if (slippage < 0)
28
- throw new Error("slippage must be a non-negative number.");
17
+ const { amount, slippage } = args;
29
18
  let result;
30
19
  if (poolType === "QUOTE") {
31
20
  result = action === "deposit"
@@ -48,11 +37,9 @@ export const getLpPriceTool = {
48
37
  name: "get_lp_price",
49
38
  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.",
50
39
  schema: {
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'"),
40
+ poolType: z.enum(["BASE", "QUOTE"]).describe("'BASE' or 'QUOTE'"),
54
41
  poolId: z.string().describe("Pool ID"),
55
- chainId: z.coerce.number().optional().describe("Optional chainId override"),
42
+ chainId: z.number().int().positive().optional().describe("Optional chainId override"),
56
43
  },
57
44
  handler: async (args) => {
58
45
  try {
@@ -7,7 +7,7 @@ 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
+ chainId: z.number().int().positive().optional().describe("Optional chainId override"),
11
11
  },
12
12
  handler: async (args) => {
13
13
  try {
@@ -26,7 +26,7 @@ export const getPoolInfoTool = {
26
26
  description: "Get pool on-chain information (reserves, utilization, etc.).",
27
27
  schema: {
28
28
  poolId: z.string().describe("Pool ID"),
29
- chainId: z.coerce.number().optional().describe("Optional chainId override"),
29
+ chainId: z.number().int().positive().optional().describe("Optional chainId override"),
30
30
  },
31
31
  handler: async (args) => {
32
32
  try {
@@ -44,8 +44,8 @@ export const getLiquidityInfoTool = {
44
44
  description: "Get pool liquidity utilization and depth information.",
45
45
  schema: {
46
46
  poolId: z.string().describe("Pool ID"),
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"),
47
+ marketPrice: z.string().regex(/^\d+$/).describe("Current market price in 30-decimal raw units"),
48
+ chainId: z.number().int().positive().optional().describe("Optional chainId override"),
49
49
  },
50
50
  handler: async (args) => {
51
51
  try {
@@ -24,17 +24,16 @@ export const getOpenOrdersTool = {
24
24
  };
25
25
  export const getOrderHistoryTool = {
26
26
  name: "get_order_history",
27
- description: "Get historical orders with optional pool filter and pagination.",
27
+ description: "Get historical orders with optional pool filter.",
28
28
  schema: {
29
29
  poolId: z.string().optional().describe("Filter by pool ID"),
30
- page: z.coerce.number().optional().describe("Page number (default 1)"),
31
- limit: z.coerce.number().optional().describe("Results per page (default 20)"),
30
+ limit: z.number().int().positive().optional().describe("Results per page (default 20)"),
32
31
  },
33
32
  handler: async (args) => {
34
33
  try {
35
34
  const { client, address } = await resolveClient();
36
35
  const chainId = getChainId();
37
- const query = { chainId, poolId: args.poolId, page: args.page ?? 1, limit: args.limit ?? 20 };
36
+ const query = { chainId, poolId: args.poolId, limit: args.limit ?? 20 };
38
37
  const result = await client.order.getOrderHistory(query, address);
39
38
  const enhancedData = (result?.data || []).map((order) => ({
40
39
  ...order,
@@ -6,7 +6,7 @@ export const getPoolLevelConfigTool = {
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
+ chainId: z.number().int().positive().optional().describe("Optional chainId override"),
10
10
  },
11
11
  handler: async (args) => {
12
12
  try {
@@ -3,17 +3,16 @@ import { resolveClient, getChainId } from "../auth/resolveClient.js";
3
3
  import { getDirectionDesc, getCloseTypeDesc } from "../utils/mappings.js";
4
4
  export const getPositionHistoryTool = {
5
5
  name: "get_position_history",
6
- description: "Get historical closed positions with optional pool filter and pagination.",
6
+ description: "Get historical closed positions with optional pool filter.",
7
7
  schema: {
8
8
  poolId: z.string().optional().describe("Filter by pool ID"),
9
- page: z.coerce.number().optional().describe("Page number (default 1)"),
10
- limit: z.coerce.number().optional().describe("Results per page (default 20)"),
9
+ limit: z.number().int().positive().optional().describe("Results per page (default 20)"),
11
10
  },
12
11
  handler: async (args) => {
13
12
  try {
14
13
  const { client, address } = await resolveClient();
15
14
  const chainId = getChainId();
16
- const query = { chainId, poolId: args.poolId, page: args.page ?? 1, limit: args.limit ?? 20 };
15
+ const query = { chainId, poolId: args.poolId, limit: args.limit ?? 20 };
17
16
  const result = await client.position.getPositionHistory(query, address);
18
17
  const enhancedData = (result?.data || []).map((pos) => ({
19
18
  ...pos,
@@ -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.coerce.number().optional().describe("Max results (default 100)"),
9
+ limit: z.number().int().positive().optional().describe("Max results (default 100)"),
10
10
  },
11
11
  handler: async (args) => {
12
12
  try {
@@ -1,23 +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";
5
4
  export const setTpSlTool = {
6
5
  name: "set_tp_sl",
7
- description: "Set take profit and stop loss prices for an open position.",
6
+ description: "Create TP/SL order using SDK-native parameters.",
8
7
  schema: {
9
8
  poolId: z.string().describe("Pool ID"),
10
9
  positionId: z.string().describe("Position ID"),
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%"),
20
- quoteToken: z.string().optional().describe("Quote token address"),
10
+ direction: z.union([z.literal(0), z.literal(1)]).describe("0 = LONG, 1 = SHORT"),
11
+ leverage: z.number().describe("Leverage"),
12
+ executionFeeToken: z.string().describe("Execution fee token address"),
13
+ tpTriggerType: z.number().int().min(0).max(2).describe("TP trigger type"),
14
+ slTriggerType: z.number().int().min(0).max(2).describe("SL trigger type"),
15
+ slippagePct: z.string().regex(/^\d+$/).describe("Slippage with 4-dec precision raw units"),
16
+ tpPrice: z.string().regex(/^\d+$/).optional().describe("TP price raw units (30 decimals)"),
17
+ tpSize: z.string().regex(/^\d+$/).optional().describe("TP size raw units"),
18
+ slPrice: z.string().regex(/^\d+$/).optional().describe("SL price raw units (30 decimals)"),
19
+ slSize: z.string().regex(/^\d+$/).optional().describe("SL size raw units"),
21
20
  },
22
21
  handler: async (args) => {
23
22
  try {
@@ -1,22 +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";
5
4
  export const updateOrderTpSlTool = {
6
5
  name: "update_order_tp_sl",
7
6
  description: "Update an existing take profit or stop loss order.",
8
7
  schema: {
9
8
  orderId: z.string().describe("The ID of the order to update"),
10
9
  marketId: z.string().describe("The market ID (config hash) for the order"),
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)"),
19
- quoteToken: z.string().optional().describe("Quote token address"),
10
+ size: z.string().regex(/^\d+$/).describe("Order size raw units"),
11
+ price: z.string().regex(/^\d+$/).describe("Order price raw units (30 decimals)"),
12
+ tpPrice: z.string().regex(/^\d+$/).describe("TP price raw units (30 decimals)"),
13
+ tpSize: z.string().regex(/^\d+$/).describe("TP size raw units"),
14
+ slPrice: z.string().regex(/^\d+$/).describe("SL price raw units (30 decimals)"),
15
+ slSize: z.string().regex(/^\d+$/).describe("SL size raw units"),
16
+ useOrderCollateral: z.boolean().describe("Whether to use order collateral"),
17
+ isTpSlOrder: z.boolean().optional().describe("Whether this is a TP/SL order"),
18
+ quoteToken: z.string().describe("Quote token address"),
20
19
  },
21
20
  handler: async (args) => {
22
21
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@michaleffffff/mcp-trading-server",
3
- "version": "2.4.0",
3
+ "version": "2.4.1",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "myx-mcp": "dist/server.js"
@@ -1,84 +0,0 @@
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
- });