@michaleffffff/mcp-trading-server 2.5.3 → 2.6.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/README.md CHANGED
@@ -62,6 +62,13 @@ Copy the example configuration:
62
62
  cp .env.example .env
63
63
  ```
64
64
 
65
+ ## Non-Auth Mode (No AccessToken)
66
+
67
+ This MCP server runs in **non-auth mode**:
68
+
69
+ - It does **not** call `myxClient.auth()` and does **not** manage AccessTokens.
70
+ - All tools are designed to work without AccessToken authentication.
71
+
65
72
  ## User Key Configuration (Safe Practices)
66
73
 
67
74
  - **Local dev**: Put `PRIVATE_KEY` in a local `.env` (ignored by `.gitignore`) and never commit it.
@@ -108,19 +115,43 @@ The server will run using `stdio` transport and can be connected by any MCP-comp
108
115
  2. **Configure env**: Copy `.env.example` to `.env` and fill in `PRIVATE_KEY`, `RPC_URL`, etc.
109
116
  3. **Start the server**: Run `npm run build` then `npm run start` (use `npm run dev` for development).
110
117
  4. **Configure your MCP client**: Set `command/args/env` in your MCP client (see Claude Desktop example below).
111
- 5. **Common flow**: Use `search_market` to get `poolId`, then `get_market_price` / `get_oracle_price` to view prices, and finally `execute_trade` to open a position (`direction: 0`=LONG, `1`=SHORT; `collateralAmount` is human‑readable).
118
+ 5. **Common flow**: Use `search_market` to get `poolId`, then optionally `get_market_price` / `get_oracle_price` to view prices, and finally use `open_position_simple` (recommended) to open a position.
112
119
 
113
120
  **Minimal open position example:**
114
121
 
115
122
  ```json
116
123
  {
117
- "poolId": "0x...",
124
+ "keyword": "BTC",
118
125
  "direction": 0,
119
126
  "collateralAmount": "100",
120
- "leverage": 5
127
+ "leverage": 5,
128
+ "orderType": "MARKET",
129
+ "autoDeposit": false
121
130
  }
122
131
  ```
123
132
 
133
+ **Dry run (no transaction) example:**
134
+
135
+ ```json
136
+ {
137
+ "keyword": "BTC",
138
+ "direction": 0,
139
+ "collateralAmount": "100",
140
+ "leverage": 5,
141
+ "orderType": "MARKET",
142
+ "autoDeposit": false,
143
+ "dryRun": true
144
+ }
145
+ ```
146
+
147
+ ## Units & Precision
148
+
149
+ - **Human by default**: For trading tools (e.g. `open_position_simple`), amounts like `"100"` mean **human-readable units**.
150
+ - **Raw units (explicit)**: Prefix with `raw:` to pass raw units exactly (e.g. `raw:100000000` for 100 USDC with 6 decimals).
151
+ - **Price**: `"price"` is human by default and will be converted to **30-decimal** format internally. Use `raw:` to pass a 30-decimal integer directly.
152
+ - **Slippage**: `slippagePct` uses 4-decimal raw units: `100` = `1%`, `50` = `0.5%`, `1000` = `10%`.
153
+ - **Note**: Some account-transfer tools (e.g. `account_deposit`, `account_withdraw`, `check_approval`) use **raw integer strings**.
154
+
124
155
  ---
125
156
 
126
157
  ## Secure Deployment Template (Systemd Example)
@@ -192,14 +223,15 @@ Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
192
223
 
193
224
  # Tools
194
225
 
195
- The server exposes 37 tools categorized for AI:
226
+ The server exposes 38 tools categorized for AI:
196
227
 
197
228
  ### Trading Operations
229
+ * **open_position_simple**: High-level open position helper (recommended). Computes price/size/tradingFee internally.
198
230
  * **execute_trade**: Execute a new trade or add to an existing position.
199
231
  * **close_position**: Close an open position.
200
232
  * **close_all_positions**: Emergency: close ALL open positions in a pool at once.
201
233
  * **cancel_order**: Cancel an open order by its order ID.
202
- * **cancel_all_orders**: Cancel all open orders at once (or a provided order ID list).
234
+ * **cancel_all_orders**: Cancel multiple open orders by order IDs (use `get_open_orders` first to get IDs).
203
235
  * **set_tp_sl**: Set take profit and stop loss prices for an open position.
204
236
  * **adjust_margin**: Adjust the margin (collateral) of an open position.
205
237
  * **get_user_trading_fee_rate**: Query current maker/taker fee rates by asset class and risk tier.
@@ -263,7 +295,7 @@ Example MCP client usage workflow:
263
295
  1. Connect to the MCP server.
264
296
  2. Read the `system://state` resource to confirm wallet connectivity.
265
297
  3. Call the `search_market` tool (e.g., `keyword: "ETH"`) to retrieve the `poolId`.
266
- 4. Call `execute_trade` passing the retrieved `poolId` to open a 10x long position.
298
+ 4. Call `open_position_simple` passing the retrieved `poolId` to open a 10x long position.
267
299
 
268
300
  Example integration scripts are located in:
269
301
  `examples/basicUsage.ts`
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.5.3" }, { capabilities: { tools: {}, resources: {}, prompts: {} } });
84
+ const server = new Server({ name: "myx-mcp-trading-server", version: "2.6.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.5.3 running (stdio, pure on-chain, prod ready)");
184
+ logger.info("🚀 MYX Trading MCP Server v2.6.0 running (stdio, pure on-chain, prod ready)");
185
185
  }
186
186
  main().catch((err) => {
187
187
  logger.error("Fatal Server Startup Error", err);
@@ -2,11 +2,12 @@ import { getChainId } from "../auth/resolveClient.js";
2
2
  import { getMarketStateDesc } from "../utils/mappings.js";
3
3
  export async function getMarketPrice(client, poolId, chainIdOverride) {
4
4
  const chainId = chainIdOverride ?? getChainId();
5
- const ticker = await client.markets.getTickerList({
5
+ const tickerRes = await client.markets.getTickerList({
6
6
  chainId,
7
7
  poolIds: [poolId],
8
8
  });
9
- return ticker?.data?.[0] ?? null;
9
+ const rows = Array.isArray(tickerRes) ? tickerRes : (tickerRes?.data ?? []);
10
+ return rows?.[0] ?? null;
10
11
  }
11
12
  export async function getOraclePrice(client, poolId, chainIdOverride) {
12
13
  const chainId = chainIdOverride ?? getChainId();
@@ -64,6 +64,7 @@ export async function openPosition(client, address, args) {
64
64
  const priceRaw = ensureUnits(args.price, 30, "price");
65
65
  const tradingFeeRaw = ensureUnits(args.tradingFee, quoteDecimals, "tradingFee");
66
66
  // --- Auto-Deposit Logic (Strict) ---
67
+ const allowAutoDeposit = args.autoDeposit !== false;
67
68
  console.log(`[tradeService] Checking marginBalance for ${address} in pool ${args.poolId}...`);
68
69
  const marginInfo = await client.account.getAccountInfo(chainId, address, args.poolId);
69
70
  let marginBalanceRaw = BigInt(0);
@@ -80,6 +81,10 @@ export async function openPosition(client, address, args) {
80
81
  }
81
82
  const requiredRaw = BigInt(collateralRaw);
82
83
  if (marginBalanceRaw < requiredRaw) {
84
+ if (!allowAutoDeposit) {
85
+ throw new Error(`Insufficient marginBalance (${marginBalanceRaw.toString()}) for required collateral (${requiredRaw.toString()}). ` +
86
+ `Deposit to trading account first (account_deposit) or retry with autoDeposit=true.`);
87
+ }
83
88
  const neededRaw = requiredRaw - marginBalanceRaw;
84
89
  console.log(`[tradeService] marginBalance (${marginBalanceRaw.toString()}) < Required (${requiredRaw.toString()}). Need to deposit: ${neededRaw.toString()}`);
85
90
  if (walletBalanceRaw < neededRaw) {
@@ -1,4 +1,5 @@
1
1
  // Tools — 交易
2
+ export { openPositionSimpleTool } from "./openPositionSimple.js";
2
3
  export { executeTradeTool } from "./executeTrade.js";
3
4
  export { cancelOrderTool } from "./cancelOrder.js";
4
5
  export { cancelAllOrdersTool } from "./cancelAllOrders.js";
@@ -0,0 +1,345 @@
1
+ import { z } from "zod";
2
+ import { OrderType } from "@myx-trade/sdk";
3
+ import { resolveClient, getChainId } from "../auth/resolveClient.js";
4
+ import { searchMarket } from "../services/marketService.js";
5
+ import { openPosition } from "../services/tradeService.js";
6
+ import { normalizeAddress } from "../utils/address.js";
7
+ import { finalizeMutationResult } from "../utils/mutationResult.js";
8
+ import { normalizeSlippagePct4dp, SLIPPAGE_PCT_4DP_DESC } from "../utils/slippage.js";
9
+ import { parseUserPrice30, parseUserUnits } from "../utils/units.js";
10
+ import { verifyTradeOutcome } from "../utils/verification.js";
11
+ const MAX_UINT256 = "115792089237316195423570985008687907853269984665640564039457584007913129639935";
12
+ function pow10(decimals) {
13
+ if (!Number.isInteger(decimals) || decimals < 0) {
14
+ throw new Error(`Invalid decimals: ${decimals}`);
15
+ }
16
+ return 10n ** BigInt(decimals);
17
+ }
18
+ function asBigint(raw, label) {
19
+ try {
20
+ return BigInt(raw);
21
+ }
22
+ catch {
23
+ throw new Error(`${label} must be an integer string.`);
24
+ }
25
+ }
26
+ function pickMarketDetail(res) {
27
+ if (!res)
28
+ return null;
29
+ if (res.data && typeof res.data === "object")
30
+ return res.data;
31
+ if (res.marketId)
32
+ return res;
33
+ return null;
34
+ }
35
+ function collectPoolRows(input) {
36
+ if (Array.isArray(input))
37
+ return input.flatMap(collectPoolRows);
38
+ if (!input || typeof input !== "object")
39
+ return [];
40
+ if (input.poolId || input.pool_id)
41
+ return [input];
42
+ return Object.values(input).flatMap(collectPoolRows);
43
+ }
44
+ function resolveOrderType(orderType) {
45
+ const raw = String(orderType ?? "MARKET").trim().toUpperCase();
46
+ if (raw === "MARKET")
47
+ return OrderType.MARKET;
48
+ if (raw === "LIMIT")
49
+ return OrderType.LIMIT;
50
+ if (raw === "STOP")
51
+ return OrderType.STOP;
52
+ throw new Error(`orderType must be one of: MARKET, LIMIT, STOP.`);
53
+ }
54
+ export const openPositionSimpleTool = {
55
+ name: "open_position_simple",
56
+ description: "High-level open position helper. Computes size/price/tradingFee and submits an increase order. Human units by default; use 'raw:' prefix for raw units.",
57
+ schema: {
58
+ poolId: z.string().optional().describe("Pool ID. Provide either poolId or keyword."),
59
+ keyword: z.string().optional().describe('Market keyword, e.g. "BTC". Provide either keyword or poolId.'),
60
+ direction: z.coerce
61
+ .number()
62
+ .pipe(z.union([z.literal(0), z.literal(1)]))
63
+ .describe("0 = LONG, 1 = SHORT"),
64
+ collateralAmount: z.coerce
65
+ .string()
66
+ .describe("Collateral amount in quote token units (human by default; 'raw:' prefix for raw)."),
67
+ leverage: z.coerce.number().int().positive().describe("Leverage (integer, e.g. 5, 10)."),
68
+ orderType: z
69
+ .enum(["MARKET", "LIMIT", "STOP"])
70
+ .optional()
71
+ .describe("Order type (default MARKET)."),
72
+ price: z.coerce
73
+ .string()
74
+ .optional()
75
+ .describe("Price (human by default; 30-dec raw with 'raw:' prefix). Required for LIMIT/STOP."),
76
+ size: z.coerce
77
+ .string()
78
+ .optional()
79
+ .describe("Position size in base token units (human by default; 'raw:' prefix for raw). If omitted, computed from collateral*leverage/price."),
80
+ slippagePct: z.coerce
81
+ .string()
82
+ .optional()
83
+ .describe(`${SLIPPAGE_PCT_4DP_DESC} Default 100 (=1%).`),
84
+ postOnly: z.coerce.boolean().optional().describe("Post-only (default false)."),
85
+ executionFeeToken: z.string().optional().describe("Execution fee token address (default quoteToken)."),
86
+ assetClass: z.coerce.number().int().nonnegative().optional().describe("Fee query assetClass (default 1)."),
87
+ riskTier: z.coerce.number().int().nonnegative().optional().describe("Fee query riskTier (default 1)."),
88
+ tradingFee: z.coerce
89
+ .string()
90
+ .optional()
91
+ .describe("Trading fee in quote token units (human by default; 'raw:' prefix for raw). If omitted, computed via getUserTradingFeeRate."),
92
+ autoApprove: z.coerce.boolean().optional().describe("If true, auto-approve quote token spend when needed (default false)."),
93
+ approveMax: z.coerce.boolean().optional().describe("If autoApprove, approve MaxUint256 (default false approves exact amount)."),
94
+ autoDeposit: z.coerce
95
+ .boolean()
96
+ .optional()
97
+ .describe("If true, auto-deposit to margin account when marginBalance is insufficient (default false)."),
98
+ dryRun: z.coerce.boolean().optional().describe("If true, only compute params; do not send a transaction."),
99
+ },
100
+ handler: async (args) => {
101
+ try {
102
+ const { client, address, signer } = await resolveClient();
103
+ const chainId = getChainId();
104
+ // 1) Resolve poolId (poolId or keyword)
105
+ let poolId = String(args.poolId ?? "").trim();
106
+ if (!poolId) {
107
+ const keyword = String(args.keyword ?? "").trim();
108
+ if (!keyword)
109
+ throw new Error("Either poolId or keyword is required.");
110
+ const markets = await searchMarket(client, keyword, 20);
111
+ if (markets.length) {
112
+ poolId = String(markets[0].poolId);
113
+ }
114
+ else {
115
+ // Fallback: some environments return null for markets.searchMarket; use api.getPoolList instead.
116
+ const poolListRes = await client.api.getPoolList().catch(() => null);
117
+ const rows = collectPoolRows(poolListRes?.data ?? poolListRes);
118
+ const kw = keyword.toUpperCase();
119
+ const match = rows.find((row) => {
120
+ if (Number(row?.state) !== 2)
121
+ return false;
122
+ const base = String(row?.baseSymbol ?? "").toUpperCase();
123
+ const fallbackPair = row?.baseSymbol && row?.quoteSymbol ? `${row.baseSymbol}/${row.quoteSymbol}` : "";
124
+ const pair = String(row?.baseQuoteSymbol ?? fallbackPair).toUpperCase();
125
+ return base === kw || pair.includes(kw);
126
+ });
127
+ if (!match?.poolId && !match?.pool_id) {
128
+ throw new Error(`No active market found for keyword: ${keyword}`);
129
+ }
130
+ poolId = String(match.poolId ?? match.pool_id);
131
+ }
132
+ }
133
+ // 2) Fetch market detail for decimals, quote token, marketId
134
+ const detailRes = await client.markets.getMarketDetail({ chainId, poolId });
135
+ const detail = pickMarketDetail(detailRes);
136
+ if (!detail) {
137
+ throw new Error(`Could not resolve market detail for poolId=${poolId}`);
138
+ }
139
+ const marketId = String(detail.marketId ?? "").trim();
140
+ if (!marketId)
141
+ throw new Error(`marketId missing from market detail for poolId=${poolId}`);
142
+ const baseDecimals = Number(detail.baseDecimals ?? 18);
143
+ const quoteDecimals = Number(detail.quoteDecimals ?? 6);
144
+ const quoteToken = String(detail.quoteToken ?? "").trim();
145
+ if (!quoteToken)
146
+ throw new Error(`quoteToken missing from market detail for poolId=${poolId}`);
147
+ // Optional: read pool level config (best-effort) for defaults like assetClass
148
+ let poolLevelConfig = null;
149
+ try {
150
+ poolLevelConfig = await client.markets.getPoolLevelConfig(poolId, chainId);
151
+ }
152
+ catch {
153
+ poolLevelConfig = null;
154
+ }
155
+ const defaultAssetClass = Number(poolLevelConfig?.levelConfig?.assetClass ?? 0);
156
+ // 3) Parse & validate primary inputs
157
+ const dir = Number(args.direction);
158
+ if (dir !== 0 && dir !== 1)
159
+ throw new Error("direction must be 0 (LONG) or 1 (SHORT).");
160
+ const collateralRaw = parseUserUnits(args.collateralAmount, quoteDecimals, "collateralAmount");
161
+ const collateralRawBig = asBigint(collateralRaw, "collateralAmount");
162
+ if (collateralRawBig <= 0n)
163
+ throw new Error("collateralAmount must be > 0.");
164
+ const maxTradeAmountHuman = String(process.env.MAX_TRADE_AMOUNT ?? "").trim();
165
+ if (maxTradeAmountHuman) {
166
+ const maxTradeRaw = parseUserUnits(maxTradeAmountHuman, quoteDecimals, "MAX_TRADE_AMOUNT");
167
+ const maxTradeRawBig = asBigint(maxTradeRaw, "MAX_TRADE_AMOUNT");
168
+ if (collateralRawBig > maxTradeRawBig) {
169
+ throw new Error(`collateralAmount exceeds MAX_TRADE_AMOUNT (collateralRaw=${collateralRawBig.toString()} > maxRaw=${maxTradeRawBig.toString()}).`);
170
+ }
171
+ }
172
+ const orderType = resolveOrderType(args.orderType);
173
+ const postOnly = Boolean(args.postOnly ?? false);
174
+ const slippagePct = normalizeSlippagePct4dp(args.slippagePct ?? "100");
175
+ const executionFeeToken = normalizeAddress(args.executionFeeToken || quoteToken, "executionFeeToken");
176
+ // 4) Determine reference price (30 decimals)
177
+ let price30;
178
+ let priceMeta = { source: "user", publishTime: null, oracleType: null, human: null };
179
+ if (orderType === OrderType.MARKET) {
180
+ try {
181
+ const oracle = await client.utils.getOraclePrice(poolId, chainId);
182
+ price30 = parseUserPrice30(oracle.price, "oraclePrice");
183
+ priceMeta = { source: "oracle", publishTime: oracle.publishTime, oracleType: oracle.oracleType, human: oracle.price };
184
+ }
185
+ catch (e) {
186
+ const tickers = await client.markets.getTickerList({ chainId, poolIds: [poolId] });
187
+ const row = Array.isArray(tickers) ? tickers[0] : tickers?.data?.[0];
188
+ if (!row?.price)
189
+ throw new Error(`Failed to fetch oracle and ticker price for poolId=${poolId}: ${e?.message || e}`);
190
+ price30 = parseUserPrice30(row.price, "marketPrice");
191
+ priceMeta = { source: "ticker", publishTime: null, oracleType: null, human: row.price };
192
+ }
193
+ }
194
+ else {
195
+ const userPrice = String(args.price ?? "").trim();
196
+ if (!userPrice)
197
+ throw new Error("price is required for LIMIT/STOP.");
198
+ price30 = parseUserPrice30(userPrice, "price");
199
+ priceMeta = { source: "user", publishTime: null, oracleType: null, human: userPrice };
200
+ }
201
+ const price30Big = asBigint(price30, "price");
202
+ if (price30Big <= 0n)
203
+ throw new Error("price must be > 0.");
204
+ // 5) Compute or parse size (base raw units)
205
+ let sizeRaw = "";
206
+ let sizeMeta = { source: "computed" };
207
+ const userSize = String(args.size ?? "").trim();
208
+ if (userSize) {
209
+ sizeRaw = parseUserUnits(userSize, baseDecimals, "size");
210
+ sizeMeta = { source: "user" };
211
+ }
212
+ else {
213
+ const notionalQuoteRaw = collateralRawBig * BigInt(args.leverage);
214
+ const numerator = notionalQuoteRaw * pow10(30 + baseDecimals);
215
+ const denominator = price30Big * pow10(quoteDecimals);
216
+ const computed = numerator / denominator;
217
+ if (computed <= 0n) {
218
+ throw new Error("Computed size is 0. Increase collateralAmount/leverage or check price.");
219
+ }
220
+ sizeRaw = computed.toString();
221
+ sizeMeta = { source: "computed", notionalQuoteRaw: notionalQuoteRaw.toString() };
222
+ }
223
+ // 6) Compute tradingFee (quote raw units)
224
+ let tradingFeeRaw = null;
225
+ let tradingFeeMeta = { source: "computed" };
226
+ const userTradingFee = String(args.tradingFee ?? "").trim();
227
+ if (userTradingFee) {
228
+ tradingFeeRaw = parseUserUnits(userTradingFee, quoteDecimals, "tradingFee");
229
+ tradingFeeMeta = { source: "user" };
230
+ }
231
+ else {
232
+ const assetClass = Number(args.assetClass ?? defaultAssetClass);
233
+ const riskTier = Number(args.riskTier ?? 0);
234
+ tradingFeeMeta = { source: "computed", assetClass, riskTier, feeRate: null, error: null };
235
+ try {
236
+ const feeRes = await client.utils.getUserTradingFeeRate(assetClass, riskTier, chainId);
237
+ if (feeRes && Number(feeRes.code) === 0 && feeRes.data) {
238
+ const rateRaw = postOnly ? feeRes.data.makerFeeRate : feeRes.data.takerFeeRate;
239
+ tradingFeeMeta.feeRate = rateRaw;
240
+ const rate = asBigint(String(rateRaw), "feeRate");
241
+ const fee = (collateralRawBig * rate) / 1000000n;
242
+ tradingFeeRaw = fee.toString();
243
+ }
244
+ else {
245
+ tradingFeeMeta.error = feeRes?.message ?? "fee_rate_unavailable";
246
+ }
247
+ }
248
+ catch (e) {
249
+ tradingFeeMeta.error = e?.message || String(e);
250
+ }
251
+ if (tradingFeeRaw === null && !args.dryRun) {
252
+ throw new Error(`Failed to fetch user trading fee rate (assetClass=${assetClass}, riskTier=${riskTier}). Provide tradingFee manually if needed.`);
253
+ }
254
+ }
255
+ const prep = {
256
+ chainId,
257
+ poolId,
258
+ marketId,
259
+ baseDecimals,
260
+ quoteDecimals,
261
+ quoteToken,
262
+ direction: dir,
263
+ collateralRaw,
264
+ sizeRaw,
265
+ price30,
266
+ leverage: Number(args.leverage),
267
+ orderType,
268
+ triggerType: 0,
269
+ timeInForce: 0,
270
+ postOnly,
271
+ slippagePct,
272
+ executionFeeToken,
273
+ tradingFeeRaw,
274
+ tradingFeeMeta,
275
+ autoDeposit: Boolean(args.autoDeposit ?? false),
276
+ priceMeta,
277
+ sizeMeta,
278
+ };
279
+ if (args.dryRun) {
280
+ return {
281
+ content: [
282
+ {
283
+ type: "text",
284
+ text: JSON.stringify({ status: "success", data: { dryRun: true, prepared: prep } }, (_, v) => typeof v === "bigint" ? v.toString() : v, 2),
285
+ },
286
+ ],
287
+ };
288
+ }
289
+ // 7) Optional approval
290
+ let approval = null;
291
+ if (args.autoApprove) {
292
+ const requiredApprovalRaw = tradingFeeRaw && /^\d+$/.test(String(tradingFeeRaw))
293
+ ? (collateralRawBig + asBigint(String(tradingFeeRaw), "tradingFee")).toString()
294
+ : collateralRaw;
295
+ const needApproval = await client.utils.needsApproval(address, chainId, quoteToken, requiredApprovalRaw);
296
+ if (needApproval) {
297
+ const approveAmount = args.approveMax ? MAX_UINT256 : requiredApprovalRaw;
298
+ const rawApprove = await client.utils.approveAuthorization({
299
+ chainId,
300
+ quoteAddress: quoteToken,
301
+ amount: approveAmount,
302
+ });
303
+ approval = await finalizeMutationResult(rawApprove, signer, "approve_authorization");
304
+ }
305
+ else {
306
+ approval = { needApproval: false };
307
+ }
308
+ }
309
+ // 8) Submit increase order using existing trade service
310
+ const raw = await openPosition(client, address, {
311
+ poolId,
312
+ positionId: "",
313
+ orderType,
314
+ triggerType: 0,
315
+ direction: dir,
316
+ collateralAmount: `raw:${collateralRaw}`,
317
+ size: `raw:${sizeRaw}`,
318
+ price: `raw:${price30}`,
319
+ timeInForce: 0,
320
+ postOnly,
321
+ slippagePct,
322
+ executionFeeToken,
323
+ leverage: Number(args.leverage),
324
+ tradingFee: `raw:${String(tradingFeeRaw)}`,
325
+ marketId,
326
+ autoDeposit: Boolean(args.autoDeposit ?? false),
327
+ });
328
+ const data = await finalizeMutationResult(raw, signer, "open_position_simple");
329
+ const txHash = data.confirmation?.txHash;
330
+ const verification = txHash ? await verifyTradeOutcome(client, address, poolId, txHash) : null;
331
+ const payload = { prepared: prep, approval, ...data, verification };
332
+ return {
333
+ content: [
334
+ {
335
+ type: "text",
336
+ text: JSON.stringify({ status: "success", data: payload }, (_, v) => (typeof v === "bigint" ? v.toString() : v), 2),
337
+ },
338
+ ],
339
+ };
340
+ }
341
+ catch (error) {
342
+ return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
343
+ }
344
+ },
345
+ };
@@ -17,10 +17,10 @@ export const setTpSlTool = {
17
17
  slippagePct: z.coerce.string().refine(isValidSlippagePct4dp, {
18
18
  message: "slippagePct must be an integer in [0, 10000] with 4-decimal precision (1 = 0.01%).",
19
19
  }).describe(SLIPPAGE_PCT_4DP_DESC),
20
- tpPrice: z.coerce.string().regex(/^\d+$/).optional().describe("TP price raw units (30 decimals)"),
21
- tpSize: z.coerce.string().regex(/^\d+$/).optional().describe("TP size raw units"),
22
- slPrice: z.coerce.string().regex(/^\d+$/).optional().describe("SL price raw units (30 decimals)"),
23
- slSize: z.coerce.string().regex(/^\d+$/).optional().describe("SL size raw units"),
20
+ tpPrice: z.coerce.string().optional().describe("TP price (raw or human-readable, 30 decimals)"),
21
+ tpSize: z.coerce.string().optional().describe("TP size (raw or human-readable)"),
22
+ slPrice: z.coerce.string().optional().describe("SL price (raw or human-readable, 30 decimals)"),
23
+ slSize: z.coerce.string().optional().describe("SL size (raw or human-readable)"),
24
24
  },
25
25
  handler: async (args) => {
26
26
  try {
@@ -8,12 +8,12 @@ export const updateOrderTpSlTool = {
8
8
  schema: {
9
9
  orderId: z.string().describe("The ID of the order to update"),
10
10
  marketId: z.string().describe("The market ID (config hash) for the order"),
11
- size: z.string().regex(/^\d+$/).describe("Order size raw units"),
12
- price: z.string().regex(/^\d+$/).describe("Order price raw units (30 decimals)"),
13
- tpPrice: z.string().regex(/^\d+$/).describe("TP price raw units (30 decimals)"),
14
- tpSize: z.string().regex(/^\d+$/).describe("TP size raw units"),
15
- slPrice: z.string().regex(/^\d+$/).describe("SL price raw units (30 decimals)"),
16
- slSize: z.string().regex(/^\d+$/).describe("SL size raw units"),
11
+ size: z.string().describe("Order size (raw or human-readable)"),
12
+ price: z.string().describe("Order price (raw or human-readable, 30 decimals)"),
13
+ tpPrice: z.string().describe("TP price (raw or human-readable, 30 decimals)"),
14
+ tpSize: z.string().describe("TP size (raw or human-readable)"),
15
+ slPrice: z.string().describe("SL price (raw or human-readable, 30 decimals)"),
16
+ slSize: z.string().describe("SL size (raw or human-readable)"),
17
17
  useOrderCollateral: z.boolean().describe("Whether to use order collateral"),
18
18
  isTpSlOrder: z.boolean().optional().describe("Whether this is a TP/SL order"),
19
19
  quoteToken: z.string().describe("Quote token address"),
@@ -1,5 +1,7 @@
1
1
  import { parseUnits } from "ethers";
2
2
  const DECIMAL_RE = /^-?\d+(\.\d+)?$/;
3
+ const RAW_PREFIX_RE = /^raw:/i;
4
+ const INTEGER_RE = /^-?\d+$/;
3
5
  function normalizeDecimal(input) {
4
6
  let value = input.trim();
5
7
  let sign = "";
@@ -21,6 +23,12 @@ export function ensureUnits(value, decimals, label = "value") {
21
23
  const str = String(value).trim();
22
24
  if (!str)
23
25
  throw new Error(`${label} is required.`);
26
+ if (RAW_PREFIX_RE.test(str)) {
27
+ const raw = str.replace(RAW_PREFIX_RE, "").trim();
28
+ if (!INTEGER_RE.test(raw))
29
+ throw new Error(`${label} must be an integer raw units string.`);
30
+ return raw;
31
+ }
24
32
  if (!DECIMAL_RE.test(str))
25
33
  throw new Error(`${label} must be a numeric string.`);
26
34
  // If it's already a very large integer (e.g. > 12 digits or > decimals digits),
@@ -36,6 +44,23 @@ export function ensureUnits(value, decimals, label = "value") {
36
44
  return str;
37
45
  }
38
46
  }
47
+ export function parseUserUnits(value, decimals, label = "value") {
48
+ const str = String(value).trim();
49
+ if (!str)
50
+ throw new Error(`${label} is required.`);
51
+ if (RAW_PREFIX_RE.test(str)) {
52
+ const raw = str.replace(RAW_PREFIX_RE, "").trim();
53
+ if (!INTEGER_RE.test(raw))
54
+ throw new Error(`${label} must be an integer raw units string.`);
55
+ return raw;
56
+ }
57
+ if (!DECIMAL_RE.test(str))
58
+ throw new Error(`${label} must be a numeric string.`);
59
+ return parseUnits(str, decimals).toString();
60
+ }
61
+ export function parseUserPrice30(value, label = "price") {
62
+ return parseUserUnits(value, 30, label);
63
+ }
39
64
  export function parseHumanUnits(value, decimals, label = "value") {
40
65
  const str = String(value).trim();
41
66
  if (!str)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@michaleffffff/mcp-trading-server",
3
- "version": "2.5.3",
3
+ "version": "2.6.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "myx-mcp": "dist/server.js"
@@ -17,7 +17,7 @@
17
17
  },
18
18
  "dependencies": {
19
19
  "@modelcontextprotocol/sdk": "^1.27.1",
20
- "@myx-trade/sdk": "^0.1.269",
20
+ "@myx-trade/sdk": "^0.1.270",
21
21
  "dotenv": "^17.3.1",
22
22
  "ethers": "^6.13.1",
23
23
  "zod": "^4.3.6"