@michaleffffff/mcp-trading-server 3.0.5 → 3.0.7

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/CHANGELOG.md CHANGED
@@ -1,5 +1,29 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.0.7 - 2026-03-18
4
+
5
+ ### Changed
6
+ - Updated server runtime version banner and MCP server version to `3.0.7`.
7
+ - Hardened `get_pool_metadata` warning output by compacting long low-level errors into concise warnings.
8
+ - Optimized `get_pool_info` read path:
9
+ - Prefer resolving a positive oracle/ticker market price and use it directly for pool info reads.
10
+ - Return clearer domain error for empty-liquidity / unresolved-price scenarios.
11
+ - Refined `get_user_trading_fee_rate` error handling:
12
+ - Return structured MCP error envelope (`INVALID_PARAM` / `SDK_READ_ERROR`) instead of raw error strings.
13
+ - Include normalized concise error messages and request context details.
14
+ - Enhanced `account_deposit` usability by making `tokenAddress` optional (defaults to `QUOTE_TOKEN_ADDRESS`).
15
+
16
+ ## 3.0.6 - 2026-03-18
17
+
18
+ ### Changed
19
+ - Upgraded SDK dependency to `@myx-trade/sdk@^1.0.2`.
20
+ - Updated client bootstrap to provide a viem-compatible `walletClient` shim (`json-rpc` account + `getAddresses/request/signMessage`) for SDK v1.0.2.
21
+ - Moved `get_trade_flow` to SDK v1.0.2 native path (`client.api.getTradeFlow`).
22
+ - Moved `account_withdraw` to SDK v1.0.2 native path (`account.updateAndWithdraw`).
23
+ - Normalized `baseToken` address before `pool.createPool` to satisfy stricter typed input.
24
+ - Updated account balance parsing to SDK v1.0.2 `getAccountInfo` fields (`freeMargin`, `walletBalance`).
25
+ - Hardened type guards for account snapshot and fee-rate parsing under SDK union return types.
26
+
3
27
  ## 3.0.5 - 2026-03-18
4
28
 
5
29
  ### Changed
package/README.md CHANGED
@@ -6,7 +6,8 @@ A production-ready MCP (Model Context Protocol) server for deep integration with
6
6
 
7
7
  # Release Notes
8
8
 
9
- - **Current release: 3.0.4**
9
+ - **Current release: 3.0.7**
10
+ - **SDK baseline**: `@myx-trade/sdk@^1.0.2` compatibility completed.
10
11
  - **Refinement**: Consolidated 40+ specialized tools into ~26 high-level unified tools.
11
12
  - **Improved UX**: Enhanced AI parameter parsing, automated unit conversion, and structured error reporting.
12
13
  - **Breaking changes**: Many low-level tools (e.g., `get_market_price`, `get_oracle_price`, `get_open_orders`) have been merged into unified counterparts.
@@ -24,6 +24,8 @@ export async function resolveClient() {
24
24
  const rpcUrl = process.env.RPC_URL || "https://rpc.sepolia.linea.build";
25
25
  const privateKey = process.env.PRIVATE_KEY;
26
26
  const chainId = Number(process.env.CHAIN_ID) || 59141;
27
+ const isTestnet = process.env.IS_TESTNET !== "false";
28
+ const isBetaMode = String(process.env.IS_BETA_MODE ?? "").trim().toLowerCase() === "true";
27
29
  const brokerAddressRaw = process.env.BROKER_ADDRESS || getDefaultBrokerByChainId(chainId);
28
30
  const quoteTokenRaw = process.env.QUOTE_TOKEN_ADDRESS || getDefaultQuoteTokenByChainId(chainId);
29
31
  const quoteDecimals = Number(process.env.QUOTE_TOKEN_DECIMALS) || 6;
@@ -42,12 +44,27 @@ export async function resolveClient() {
42
44
  // Inject the EIP-1193 mock so SDK can sign transactions seamlessly
43
45
  const { injectBrowserProviderMock } = await import("../utils/injectProvider.js");
44
46
  injectBrowserProviderMock(chainId, provider, signer);
47
+ const ethereumProvider = globalThis.window.ethereum;
48
+ const walletClient = {
49
+ transport: ethereumProvider,
50
+ chain: { id: chainId },
51
+ account: { address: signer.address, type: "json-rpc" },
52
+ getAddresses: async () => [signer.address],
53
+ request: async (args) => ethereumProvider.request(args),
54
+ signMessage: async ({ message }) => {
55
+ const payload = typeof message === "string"
56
+ ? message
57
+ : (message?.raw ?? message?.message ?? "");
58
+ return signer.signMessage(payload);
59
+ },
60
+ };
45
61
  const client = new MyxClient({
46
62
  chainId,
47
63
  signer: signer,
48
64
  brokerAddress,
49
- isTestnet: process.env.IS_TESTNET !== "false",
50
- walletClient: { transport: globalThis.window.ethereum }
65
+ isTestnet,
66
+ isBetaMode,
67
+ walletClient: walletClient
51
68
  });
52
69
  cached = { client, address: signer.address, signer, chainId, quoteToken, quoteDecimals };
53
70
  return cached;
package/dist/server.js CHANGED
@@ -370,7 +370,7 @@ function zodSchemaToJsonSchema(zodSchema) {
370
370
  };
371
371
  }
372
372
  // ─── MCP Server ───
373
- const server = new Server({ name: "myx-mcp-trading-server", version: "3.0.5" }, { capabilities: { tools: {}, resources: {}, prompts: {} } });
373
+ const server = new Server({ name: "myx-mcp-trading-server", version: "3.0.7" }, { capabilities: { tools: {}, resources: {}, prompts: {} } });
374
374
  // List tools
375
375
  server.setRequestHandler(ListToolsRequestSchema, async () => {
376
376
  return {
@@ -491,7 +491,7 @@ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
491
491
  async function main() {
492
492
  const transport = new StdioServerTransport();
493
493
  await server.connect(transport);
494
- logger.info("🚀 MYX Trading MCP Server v3.0.5 running (stdio, pure on-chain, prod ready)");
494
+ logger.info("🚀 MYX Trading MCP Server v3.0.7 running (stdio, pure on-chain, prod ready)");
495
495
  }
496
496
  main().catch((err) => {
497
497
  logger.error("Fatal Server Startup Error", err);
@@ -2,6 +2,7 @@ import { pool, quote, base } from "@myx-trade/sdk";
2
2
  import { getChainId, resolveClient } from "../auth/resolveClient.js";
3
3
  import { extractErrorMessage } from "../utils/errorMessage.js";
4
4
  import { ensureUnits } from "../utils/units.js";
5
+ import { normalizeAddress } from "../utils/address.js";
5
6
  function isDivideByZeroError(message) {
6
7
  const lower = message.toLowerCase();
7
8
  return (lower.includes("divide_by_zero") ||
@@ -19,74 +20,74 @@ function toPositiveBigint(input) {
19
20
  return null;
20
21
  }
21
22
  }
23
+ async function resolvePositiveMarketPrice30(client, poolId, chainId) {
24
+ if (!client)
25
+ return null;
26
+ try {
27
+ const oracle = await client.utils?.getOraclePrice?.(poolId, chainId);
28
+ const byValue = toPositiveBigint(oracle?.value);
29
+ const byPrice = toPositiveBigint(oracle?.price);
30
+ if (byValue)
31
+ return byValue;
32
+ if (byPrice)
33
+ return byPrice;
34
+ }
35
+ catch {
36
+ }
37
+ try {
38
+ const tickerRes = await client.markets?.getTickerList?.({ chainId, poolIds: [poolId] });
39
+ const row = Array.isArray(tickerRes) ? tickerRes[0] : tickerRes?.data?.[0];
40
+ if (row?.price) {
41
+ const tickerRaw = ensureUnits(row.price, 30, "ticker price");
42
+ const byTicker = toPositiveBigint(tickerRaw);
43
+ if (byTicker)
44
+ return byTicker;
45
+ }
46
+ }
47
+ catch {
48
+ }
49
+ return null;
50
+ }
22
51
  /**
23
52
  * 创建合约市场池子
24
53
  */
25
54
  export async function createPool(baseToken, marketId) {
26
55
  await resolveClient();
27
56
  const chainId = getChainId();
28
- return pool.createPool({ chainId, baseToken, marketId });
57
+ return pool.createPool({ chainId, baseToken: normalizeAddress(baseToken, "baseToken"), marketId });
29
58
  }
30
59
  /**
31
60
  * 获取池子信息
32
61
  */
33
62
  export async function getPoolInfo(poolId, chainIdOverride, clientOverride) {
34
63
  const chainId = chainIdOverride ?? getChainId();
35
- let needOracleFallback = false;
64
+ const client = clientOverride ?? (await resolveClient()).client;
36
65
  try {
37
- const direct = await pool.getPoolInfo(chainId, poolId);
38
- if (direct)
39
- return direct;
40
- needOracleFallback = true;
66
+ const marketPrice30 = await resolvePositiveMarketPrice30(client, poolId, chainId);
67
+ if (marketPrice30 && marketPrice30 > 0n) {
68
+ const withPrice = await pool.getPoolInfo(chainId, poolId, marketPrice30);
69
+ if (withPrice)
70
+ return withPrice;
71
+ }
41
72
  }
42
73
  catch (error) {
43
74
  const message = extractErrorMessage(error);
44
75
  if (!isDivideByZeroError(message)) {
45
76
  throw new Error(`get_pool_info failed: ${message}`);
46
77
  }
47
- needOracleFallback = true;
48
- }
49
- if (!needOracleFallback)
50
- return undefined;
51
- const client = clientOverride ?? (await resolveClient()).client;
52
- if (!client?.utils?.getOraclePrice) {
53
- throw new Error("get_pool_info failed and oracle fallback is unavailable (client.utils.getOraclePrice missing).");
54
78
  }
55
- let oracleRaw = 0n;
56
79
  try {
57
- const oracle = await client.utils.getOraclePrice(poolId, chainId);
58
- const byValue = toPositiveBigint(oracle?.value);
59
- const byPrice = toPositiveBigint(oracle?.price);
60
- oracleRaw = byValue ?? byPrice ?? 0n;
80
+ const direct = await pool.getPoolInfo(chainId, poolId);
81
+ if (direct)
82
+ return direct;
83
+ throw new Error(`Pool info for ${poolId} returned undefined.`);
61
84
  }
62
85
  catch (error) {
63
- throw new Error(`get_pool_info fallback failed to fetch oracle price: ${extractErrorMessage(error)}`);
64
- }
65
- if (oracleRaw <= 0n) {
66
- try {
67
- const tickerRes = await client.markets.getTickerList({ chainId, poolIds: [poolId] });
68
- const row = Array.isArray(tickerRes) ? tickerRes[0] : tickerRes?.data?.[0];
69
- if (row?.price) {
70
- const tickerRaw = ensureUnits(row.price, 30, "ticker price");
71
- const byTicker = toPositiveBigint(tickerRaw);
72
- oracleRaw = byTicker ?? 0n;
73
- }
74
- }
75
- catch {
76
- }
77
- }
78
- if (oracleRaw <= 0n) {
79
- throw new Error("get_pool_info fallback requires a positive oracle/ticker price, but both resolved to 0.");
80
- }
81
- try {
82
- const retried = await pool.getPoolInfo(chainId, poolId, oracleRaw);
83
- if (!retried) {
84
- throw new Error(`Pool info for ${poolId} returned undefined after oracle-price retry.`);
86
+ const message = extractErrorMessage(error);
87
+ if (isDivideByZeroError(message)) {
88
+ throw new Error("get_pool_info unavailable: pool reserves are currently empty or market price context is unresolved.");
85
89
  }
86
- return retried;
87
- }
88
- catch (error) {
89
- throw new Error(`get_pool_info failed after oracle-price retry: ${extractErrorMessage(error)}`);
90
+ throw new Error(`get_pool_info failed: ${message}`);
90
91
  }
91
92
  }
92
93
  /**
@@ -93,6 +93,17 @@ function parseDecimals(value, fallback) {
93
93
  function normalizeIdentifier(value) {
94
94
  return String(value ?? "").trim().toLowerCase();
95
95
  }
96
+ function toBigIntOrZero(value) {
97
+ try {
98
+ const text = String(value ?? "").trim();
99
+ if (!text)
100
+ return 0n;
101
+ return BigInt(text);
102
+ }
103
+ catch {
104
+ return 0n;
105
+ }
106
+ }
96
107
  async function resolveDecimalsForUpdateOrder(client, chainId, marketId, poolIdHint) {
97
108
  let baseDecimals = 18;
98
109
  let quoteDecimals = getQuoteDecimals();
@@ -193,16 +204,10 @@ export async function openPosition(client, address, args) {
193
204
  console.log(`[tradeService] Checking marginBalance for ${address} in pool ${args.poolId}...`);
194
205
  const marginInfo = await client.account.getAccountInfo(chainId, address, args.poolId);
195
206
  let marginBalanceRaw = BigInt(0);
196
- let walletBalanceRaw = BigInt(0); // Using availableMargin as wallet balance per user
197
- if (marginInfo?.code === 0) {
198
- if (Array.isArray(marginInfo.data)) {
199
- marginBalanceRaw = BigInt(marginInfo.data[0] || "0");
200
- walletBalanceRaw = BigInt(marginInfo.data[1] || "0");
201
- }
202
- else {
203
- marginBalanceRaw = BigInt(marginInfo.data?.marginBalance || "0");
204
- walletBalanceRaw = BigInt(marginInfo.data?.availableMargin || "0");
205
- }
207
+ let walletBalanceRaw = BigInt(0);
208
+ if (marginInfo?.code === 0 && marginInfo?.data) {
209
+ marginBalanceRaw = toBigIntOrZero(marginInfo.data.freeMargin);
210
+ walletBalanceRaw = toBigIntOrZero(marginInfo.data.walletBalance);
206
211
  }
207
212
  const requiredRaw = BigInt(collateralRaw);
208
213
  if (marginBalanceRaw < requiredRaw) {
@@ -213,15 +218,15 @@ export async function openPosition(client, address, args) {
213
218
  const neededRaw = requiredRaw - marginBalanceRaw;
214
219
  console.log(`[tradeService] marginBalance (${marginBalanceRaw.toString()}) < Required (${requiredRaw.toString()}). Need to deposit: ${neededRaw.toString()}`);
215
220
  if (walletBalanceRaw < neededRaw) {
216
- // Also check real wallet balance just in case user's availableMargin is truly empty
221
+ // Also check real wallet balance just in case account info wallet field is stale.
217
222
  const realWalletRes = await client.account.getWalletQuoteTokenBalance(chainId, address);
218
223
  const realWalletRaw = BigInt(realWalletRes?.data || "0");
219
224
  if (realWalletRaw < neededRaw) {
220
- throw new Error(`Insufficient funds: marginBalance (${marginBalanceRaw.toString()}) + availableMargin (as wallet: ${walletBalanceRaw.toString()}) + real wallet (${realWalletRaw.toString()}) is less than required collateral (${requiredRaw.toString()}).`);
225
+ throw new Error(`Insufficient funds: marginBalance (${marginBalanceRaw.toString()}) + walletBalance (${walletBalanceRaw.toString()}) + realWallet (${realWalletRaw.toString()}) is less than required collateral (${requiredRaw.toString()}).`);
221
226
  }
222
227
  walletBalanceRaw = realWalletRaw;
223
228
  }
224
- console.log(`[tradeService] Depositing ${neededRaw.toString()} ${poolData.quoteSymbol} from wallet (availableMargin)...`);
229
+ console.log(`[tradeService] Depositing ${neededRaw.toString()} ${poolData.quoteSymbol} from wallet...`);
225
230
  const depositRaw = await client.account.deposit({
226
231
  amount: neededRaw.toString(),
227
232
  tokenAddress: poolData.quoteToken,
@@ -1,57 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { resolveClient, getChainId } from "../auth/resolveClient.js";
3
3
  import { getTradeFlowTypeDesc } from "../utils/mappings.js";
4
- function readErrorMessage(error) {
5
- if (error instanceof Error)
6
- return error.message;
7
- if (typeof error === "string")
8
- return error;
9
- if (error && typeof error === "object" && "message" in error) {
10
- return String(error.message);
11
- }
12
- return String(error ?? "unknown error");
13
- }
14
- function assertSdkReadSuccess(result, actionName) {
15
- if (!result || typeof result !== "object" || Array.isArray(result))
16
- return;
17
- if (!Object.prototype.hasOwnProperty.call(result, "code"))
18
- return;
19
- const code = Number(result.code);
20
- if (!Number.isFinite(code) || code === 0)
21
- return;
22
- const payload = result.data;
23
- const hasPayload = payload !== null &&
24
- payload !== undefined &&
25
- (!Array.isArray(payload) || payload.length > 0);
26
- if (code > 0 && hasPayload) {
27
- // Keep aligned with server-side read normalization:
28
- // positive non-zero code with usable payload is warning-like.
29
- return;
30
- }
31
- const message = String(result.msg ?? result.message ?? "unknown error");
32
- throw new Error(`${actionName} failed: code=${code}, msg=${message}`);
33
- }
34
- function normalizeAccountInfo(result) {
35
- if (!(result && result.code === 0 && Array.isArray(result.data))) {
36
- return result;
37
- }
38
- const values = result.data;
39
- return {
40
- ...result,
41
- data: {
42
- freeMargin: values[0],
43
- walletBalance: values[1],
44
- freeBaseAmount: values[2],
45
- baseProfit: values[3],
46
- quoteProfit: values[4],
47
- reservedAmount: values[5],
48
- releaseTime: values[6],
49
- tradeableMargin: (BigInt(values[0]) + BigInt(values[1]) + (BigInt(values[6]) === 0n ? BigInt(values[4]) : 0n)).toString(),
50
- baseProfitStatus: "base token to be unlocked",
51
- quoteProfitStatus: BigInt(values[6]) > 0n ? "quote token to be unlocked" : "quote token unlocked/available"
52
- }
53
- };
54
- }
55
4
  export const getTradeFlowTool = {
56
5
  name: "get_trade_flow",
57
6
  description: "[ACCOUNT] Get account trade flow / transaction history.",
@@ -64,7 +13,10 @@ export const getTradeFlowTool = {
64
13
  const { client, address } = await resolveClient();
65
14
  const chainId = getChainId();
66
15
  const query = { chainId, poolId: args.poolId, limit: args.limit ?? 20 };
67
- const result = await client.account.getTradeFlow(query, address);
16
+ const accessToken = typeof client?.getAccessToken === "function"
17
+ ? (await client.getAccessToken()) || ""
18
+ : "";
19
+ const result = await client.api.getTradeFlow({ ...query, address, accessToken });
68
20
  const enhancedData = (result?.data || []).map((flow) => ({
69
21
  ...flow,
70
22
  typeDesc: getTradeFlowTypeDesc(flow.type)
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { resolveClient, getChainId } from "../auth/resolveClient.js";
2
+ import { resolveClient, getChainId, getQuoteToken } from "../auth/resolveClient.js";
3
3
  import { normalizeAddress } from "../utils/address.js";
4
4
  import { finalizeMutationResult } from "../utils/mutationResult.js";
5
5
  const MAX_UINT256 = "115792089237316195423570985008687907853269984665640564039457584007913129639935";
@@ -42,7 +42,7 @@ export const accountDepositTool = {
42
42
  description: "[ACCOUNT] Deposit funds from wallet into the MYX trading account.",
43
43
  schema: {
44
44
  amount: z.union([z.string(), z.number()]).describe("Amount to deposit (human-readable or raw units)"),
45
- tokenAddress: z.string().describe("Token address"),
45
+ tokenAddress: z.string().optional().describe("Token address (optional, default: QUOTE_TOKEN_ADDRESS)"),
46
46
  autoApprove: z.coerce.boolean().optional().describe("If true, auto-approve token allowance when needed (default false)."),
47
47
  approveMax: z.coerce.boolean().optional().describe("If autoApprove=true, approve MaxUint256 instead of exact amount."),
48
48
  },
@@ -50,7 +50,8 @@ export const accountDepositTool = {
50
50
  try {
51
51
  const { client, signer, address } = await resolveClient();
52
52
  const chainId = getChainId();
53
- const tokenAddress = normalizeAddress(args.tokenAddress, "tokenAddress");
53
+ const tokenAddressInput = String(args.tokenAddress ?? "").trim() || getQuoteToken();
54
+ const tokenAddress = normalizeAddress(tokenAddressInput, "tokenAddress");
54
55
  // For deposit, we default to quote decimals (6) as it's the most common use case.
55
56
  // ensureUnits handles 'raw:' prefix if absolute precision is needed.
56
57
  const { ensureUnits } = await import("../utils/units.js");
@@ -128,13 +129,7 @@ export const accountWithdrawTool = {
128
129
  throw new Error(`Account has locked funds until releaseTime=${formatUnixTimestamp(releaseTime)}. ` +
129
130
  `Retry after unlock or reduce withdraw amount.`);
130
131
  }
131
- const raw = await client.account.withdraw({
132
- chainId,
133
- receiver: address,
134
- amount,
135
- poolId: args.poolId,
136
- isQuoteToken: args.isQuoteToken,
137
- });
132
+ const raw = await client.account.updateAndWithdraw(address, args.poolId, Boolean(args.isQuoteToken), amount, chainId);
138
133
  const data = await finalizeMutationResult(raw, signer, "account_withdraw");
139
134
  const preflight = {
140
135
  requestedAmountRaw: amountRaw.toString(),
@@ -3,6 +3,17 @@ import { resolveClient, getChainId } from "../auth/resolveClient.js";
3
3
  import { resolvePool } from "../services/marketService.js";
4
4
  import { parseUserUnits } from "../utils/units.js";
5
5
  import { ethers } from "ethers";
6
+ function toBigIntOrZero(value) {
7
+ try {
8
+ const text = String(value ?? "").trim();
9
+ if (!text)
10
+ return 0n;
11
+ return BigInt(text);
12
+ }
13
+ catch {
14
+ return 0n;
15
+ }
16
+ }
6
17
  export const checkAccountReadyTool = {
7
18
  name: "check_account_ready",
8
19
  description: "[TRADE] Check if the account has sufficient funds (margin + wallet) for a planned trade.",
@@ -22,16 +33,10 @@ export const checkAccountReadyTool = {
22
33
  const quoteSymbol = detail?.quoteSymbol || "USDC";
23
34
  const marginInfo = await client.account.getAccountInfo(chainId, address, poolId);
24
35
  let marginBalanceRaw = 0n;
25
- let availableMarginRaw = 0n;
26
- if (marginInfo?.code === 0) {
27
- if (Array.isArray(marginInfo.data)) {
28
- marginBalanceRaw = BigInt(marginInfo.data[0] || "0");
29
- availableMarginRaw = BigInt(marginInfo.data[1] || "0");
30
- }
31
- else {
32
- marginBalanceRaw = BigInt(marginInfo.data?.marginBalance || "0");
33
- availableMarginRaw = BigInt(marginInfo.data?.availableMargin || "0");
34
- }
36
+ let accountWalletBalanceRaw = 0n;
37
+ if (marginInfo?.code === 0 && marginInfo?.data) {
38
+ marginBalanceRaw = toBigIntOrZero(marginInfo.data.freeMargin);
39
+ accountWalletBalanceRaw = toBigIntOrZero(marginInfo.data.walletBalance);
35
40
  }
36
41
  const walletRes = await client.account.getWalletQuoteTokenBalance(chainId, address);
37
42
  const walletBalanceRaw = BigInt(walletRes?.data || "0");
@@ -55,6 +60,7 @@ export const checkAccountReadyTool = {
55
60
  hasEnoughInMargin: marginBalanceRaw >= requiredRaw,
56
61
  needDepositFromWallet,
57
62
  walletSufficientForDeposit: walletSufficient,
63
+ accountInfoWalletBalance: format(accountWalletBalanceRaw),
58
64
  quoteSymbol
59
65
  }
60
66
  }
@@ -5,23 +5,30 @@ import { finalizeMutationResult } from "../utils/mutationResult.js";
5
5
  import { SLIPPAGE_PCT_4DP_DESC } from "../utils/slippage.js";
6
6
  import { verifyTradeOutcome } from "../utils/verification.js";
7
7
  import { mapDirection, mapOrderType, mapTriggerType } from "../utils/mappings.js";
8
+ import { parseUserUnits } from "../utils/units.js";
8
9
  const FULL_CLOSE_MARKERS = new Set(["ALL", "FULL", "MAX"]);
9
10
  const INTEGER_RE = /^\d+$/;
10
11
  function wantsFullCloseMarker(input) {
11
12
  const value = String(input ?? "").trim().toUpperCase();
12
13
  return FULL_CLOSE_MARKERS.has(value);
13
14
  }
14
- function readRawPositionField(position, primary, fallback) {
15
- const first = String(position?.[primary] ?? "").trim();
16
- if (INTEGER_RE.test(first))
17
- return first;
18
- if (fallback) {
19
- const second = String(position?.[fallback] ?? "").trim();
20
- if (INTEGER_RE.test(second))
21
- return second;
15
+ function readFirstPositionField(position, fields) {
16
+ for (const field of fields) {
17
+ const value = String(position?.[field] ?? "").trim();
18
+ if (value)
19
+ return value;
22
20
  }
23
21
  return "";
24
22
  }
23
+ function resolvePositionRaw(position, rawFields, humanFields, decimals, label) {
24
+ const raw = readFirstPositionField(position, rawFields);
25
+ if (INTEGER_RE.test(raw))
26
+ return raw;
27
+ const human = readFirstPositionField(position, humanFields);
28
+ if (!human)
29
+ return "";
30
+ return parseUserUnits(human, decimals, label);
31
+ }
25
32
  export const closePositionTool = {
26
33
  name: "close_position",
27
34
  description: "[TRADE] Create a decrease order (close or reduce position) using SDK-native parameters.",
@@ -67,14 +74,14 @@ export const closePositionTool = {
67
74
  throw new Error(`Could not find live position snapshot for positionId=${preparedArgs.positionId} in poolId=${poolId}.`);
68
75
  }
69
76
  if (needAutoSize) {
70
- const rawSize = readRawPositionField(target, "size", "positionSize");
77
+ const rawSize = resolvePositionRaw(target, ["sizeRaw", "positionSizeRaw"], ["size", "positionSize"], Number(poolData.baseDecimals ?? 18), "position.size");
71
78
  if (!rawSize || rawSize === "0") {
72
79
  throw new Error(`Resolved position size is empty/zero for positionId=${preparedArgs.positionId}.`);
73
80
  }
74
81
  preparedArgs.size = `raw:${rawSize}`;
75
82
  }
76
83
  if (needAutoCollateral) {
77
- const rawCollateral = readRawPositionField(target, "collateral", "collateralAmount");
84
+ const rawCollateral = resolvePositionRaw(target, ["collateralRaw", "collateralAmountRaw"], ["collateral", "collateralAmount"], Number(poolData.quoteDecimals ?? 6), "position.collateralAmount");
78
85
  if (!rawCollateral) {
79
86
  throw new Error(`Resolved position collateral is empty for positionId=${preparedArgs.positionId}.`);
80
87
  }
@@ -1,5 +1,11 @@
1
1
  import { z } from "zod";
2
2
  import { resolveClient, getChainId } from "../auth/resolveClient.js";
3
+ function unwrapData(result) {
4
+ if (result && typeof result === "object" && "data" in result) {
5
+ return result.data;
6
+ }
7
+ return result;
8
+ }
3
9
  export const getAccountSnapshotTool = {
4
10
  name: "get_account_snapshot",
5
11
  description: "[ACCOUNT] Get unified account snapshot (balances, trading metrics, and VIP tier).",
@@ -23,14 +29,14 @@ export const getAccountSnapshotTool = {
23
29
  getMarginBalance(client, address, args.poolId, chainId).catch(() => null)
24
30
  ]);
25
31
  tradingAccount = {
26
- info: info?.data || info,
27
- margin: margin?.data || margin
32
+ info: unwrapData(info),
33
+ margin: unwrapData(margin)
28
34
  };
29
35
  }
30
36
  const results = {
31
- wallet: walletRes?.data || walletRes,
37
+ wallet: unwrapData(walletRes),
32
38
  tradingAccount,
33
- vip: vipRes?.data || vipRes,
39
+ vip: unwrapData(vipRes),
34
40
  };
35
41
  return { content: [{ type: "text", text: JSON.stringify({ status: "success", data: results }, (_, v) => typeof v === 'bigint' ? v.toString() : v, 2) }] };
36
42
  }
@@ -17,6 +17,18 @@ function normalizeMarketPrice30Input(value) {
17
17
  }
18
18
  return parseUserPrice30(text, "marketPrice");
19
19
  }
20
+ function compactWarning(scope, err) {
21
+ const raw = extractErrorMessage(err);
22
+ const flat = raw.replace(/\s+/g, " ").trim();
23
+ const lower = flat.toLowerCase();
24
+ if (lower.includes("division or modulo by zero") || lower.includes("panic code 0x12")) {
25
+ return `${scope}: unavailable for this pool at current liquidity/price context.`;
26
+ }
27
+ if (flat.length > 220) {
28
+ return `${scope}: ${flat.slice(0, 220)}...`;
29
+ }
30
+ return `${scope}: ${flat}`;
31
+ }
20
32
  export const getPoolMetadataTool = {
21
33
  name: "get_pool_metadata",
22
34
  description: "[MARKET] Get comprehensive metadata for a pool (market detail, on-chain info, liquidity, and limits).",
@@ -40,14 +52,14 @@ export const getPoolMetadataTool = {
40
52
  results.marketDetail = await getMarketDetail(client, poolId, chainId);
41
53
  }
42
54
  catch (err) {
43
- errors.push(`marketDetail: ${extractErrorMessage(err)}`);
55
+ errors.push(compactWarning("marketDetail", err));
44
56
  }
45
57
  // 2. Pool Info (Reserves, Utilization)
46
58
  try {
47
59
  results.poolInfo = await getPoolInfo(poolId, chainId, client);
48
60
  }
49
61
  catch (err) {
50
- errors.push(`poolInfo: ${extractErrorMessage(err)}`);
62
+ errors.push(compactWarning("poolInfo", err));
51
63
  }
52
64
  // 3. Optional: Liquidity Info
53
65
  if (args.includeLiquidity) {
@@ -58,7 +70,7 @@ export const getPoolMetadataTool = {
58
70
  results.liquidityInfo = await getLiquidityInfo(client, poolId, price, chainId);
59
71
  }
60
72
  catch (err) {
61
- errors.push(`liquidityInfo: ${extractErrorMessage(err)}`);
73
+ errors.push(compactWarning("liquidityInfo", err));
62
74
  }
63
75
  }
64
76
  // 4. Optional: Config / Limits
@@ -68,7 +80,7 @@ export const getPoolMetadataTool = {
68
80
  results.levelConfig = await getPoolLevelConfig(client, poolId, chainId);
69
81
  }
70
82
  catch (err) {
71
- errors.push(`levelConfig: ${extractErrorMessage(err)}`);
83
+ errors.push(compactWarning("levelConfig", err));
72
84
  }
73
85
  }
74
86
  if (errors.length > 0) {
@@ -1,5 +1,62 @@
1
1
  import { z } from "zod";
2
2
  import { resolveClient, getChainId } from "../auth/resolveClient.js";
3
+ import { extractErrorMessage } from "../utils/errorMessage.js";
4
+ function compactMessage(message) {
5
+ const flat = String(message ?? "").replace(/\s+/g, " ").trim();
6
+ if (!flat)
7
+ return "Unknown fee-rate read error.";
8
+ if (flat.length <= 240)
9
+ return flat;
10
+ return `${flat.slice(0, 240)}...`;
11
+ }
12
+ async function withMutedSdkFeeRateLogs(runner) {
13
+ const original = console.error;
14
+ console.error = (...args) => {
15
+ const first = args?.[0];
16
+ const firstText = typeof first === "string"
17
+ ? first
18
+ : first instanceof Error
19
+ ? first.message
20
+ : String(first ?? "");
21
+ const lower = firstText.toLowerCase();
22
+ if (lower.includes("myx-sdk-error") ||
23
+ (lower.includes("getuserfeerate") && (lower.includes("revert") || lower.includes("contractfunctionexecutionerror")))) {
24
+ return;
25
+ }
26
+ original(...args);
27
+ };
28
+ try {
29
+ return await runner();
30
+ }
31
+ finally {
32
+ console.error = original;
33
+ }
34
+ }
35
+ function buildErrorPayload(args, messageLike) {
36
+ const message = compactMessage(extractErrorMessage(messageLike));
37
+ const lower = message.toLowerCase();
38
+ const code = lower.includes("invalidparameter") || lower.includes("invalid parameter")
39
+ ? "INVALID_PARAM"
40
+ : "SDK_READ_ERROR";
41
+ const hint = code === "INVALID_PARAM"
42
+ ? "Check assetClass/riskTier for this pool and retry."
43
+ : "Fee tier may be unavailable for current account/market context. Retry with valid assetClass/riskTier or provide tradingFee manually.";
44
+ return {
45
+ status: "error",
46
+ error: {
47
+ tool: "get_user_trading_fee_rate",
48
+ code,
49
+ message,
50
+ hint,
51
+ action: "Adjust params/context and retry.",
52
+ details: {
53
+ assetClass: args.assetClass,
54
+ riskTier: args.riskTier,
55
+ chainId: args.chainId ?? getChainId(),
56
+ },
57
+ },
58
+ };
59
+ }
3
60
  export const getUserTradingFeeRateTool = {
4
61
  name: "get_user_trading_fee_rate",
5
62
  description: "[TRADE] Get maker/taker fee rates for a given assetClass and riskTier.",
@@ -12,11 +69,17 @@ export const getUserTradingFeeRateTool = {
12
69
  try {
13
70
  const { client } = await resolveClient();
14
71
  const chainId = args.chainId ?? getChainId();
15
- const result = await client.utils.getUserTradingFeeRate(args.assetClass, args.riskTier, chainId);
72
+ const result = await withMutedSdkFeeRateLogs(() => client.utils.getUserTradingFeeRate(args.assetClass, args.riskTier, chainId));
73
+ const maybeCode = Number(result?.code);
74
+ if (Number.isFinite(maybeCode) && maybeCode !== 0) {
75
+ const body = buildErrorPayload(args, result?.msg ?? result?.message ?? result);
76
+ return { content: [{ type: "text", text: JSON.stringify(body, (_, v) => typeof v === "bigint" ? v.toString() : v, 2) }], isError: true };
77
+ }
16
78
  return { content: [{ type: "text", text: JSON.stringify({ status: "success", data: result }, (_, v) => typeof v === "bigint" ? v.toString() : v, 2) }] };
17
79
  }
18
80
  catch (error) {
19
- return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
81
+ const body = buildErrorPayload(args, error);
82
+ return { content: [{ type: "text", text: JSON.stringify(body, (_, v) => typeof v === "bigint" ? v.toString() : v, 2) }], isError: true };
20
83
  }
21
84
  },
22
85
  };
@@ -1,6 +1,8 @@
1
1
  import { z } from "zod";
2
2
  import { resolveClient, getChainId } from "../auth/resolveClient.js";
3
3
  import { finalizeMutationResult } from "../utils/mutationResult.js";
4
+ import { parseUserUnits } from "../utils/units.js";
5
+ const INTEGER_RE = /^\d+$/;
4
6
  function normalizeDirectionInput(value) {
5
7
  if (value === undefined || value === null || value === "")
6
8
  return undefined;
@@ -60,6 +62,44 @@ async function findOrderSnapshot(client, address, chainId, orderId, poolId) {
60
62
  return null;
61
63
  }
62
64
  }
65
+ async function findPositionSnapshot(client, address, poolId, positionId) {
66
+ const targetPos = String(positionId ?? "").trim().toLowerCase();
67
+ const targetPool = String(poolId ?? "").trim().toLowerCase();
68
+ if (!targetPos || !targetPool)
69
+ return null;
70
+ try {
71
+ const positionsRes = await client.position.listPositions(address);
72
+ const positions = Array.isArray(positionsRes?.data) ? positionsRes.data : [];
73
+ return positions.find((position) => {
74
+ const pid = String(position?.positionId ?? position?.position_id ?? "").trim().toLowerCase();
75
+ const pool = String(position?.poolId ?? position?.pool_id ?? "").trim().toLowerCase();
76
+ return pid === targetPos && pool === targetPool;
77
+ }) ?? null;
78
+ }
79
+ catch {
80
+ return null;
81
+ }
82
+ }
83
+ function resolvePositionSizeRaw(positionSnapshot, baseDecimals) {
84
+ const rawCandidates = [positionSnapshot?.sizeRaw, positionSnapshot?.positionSizeRaw];
85
+ for (const value of rawCandidates) {
86
+ const text = String(value ?? "").trim();
87
+ if (INTEGER_RE.test(text))
88
+ return text;
89
+ }
90
+ const humanCandidates = [positionSnapshot?.size, positionSnapshot?.positionSize];
91
+ for (const value of humanCandidates) {
92
+ const text = String(value ?? "").trim();
93
+ if (!text)
94
+ continue;
95
+ try {
96
+ return parseUserUnits(text, baseDecimals, "position.size");
97
+ }
98
+ catch {
99
+ }
100
+ }
101
+ return "";
102
+ }
63
103
  export const manageTpSlTool = {
64
104
  name: "manage_tp_sl",
65
105
  description: "[TRADE] Set or update Take Profit (TP) and Stop Loss (SL) for a position.",
@@ -87,12 +127,16 @@ export const manageTpSlTool = {
87
127
  const direction = normalizeDirectionInput(args.direction);
88
128
  const slippagePct = args.slippagePct ?? "100";
89
129
  const { setPositionTpSl, updateOrderTpSl } = await import("../services/tradeService.js");
130
+ const { getMarketDetail } = await import("../services/marketService.js");
131
+ const marketRes = await getMarketDetail(client, args.poolId, chainId);
132
+ const market = marketRes?.data ?? marketRes;
133
+ if (!market?.marketId) {
134
+ throw new Error(`Could not resolve market metadata for poolId=${args.poolId}.`);
135
+ }
136
+ const baseDecimals = Number(market.baseDecimals ?? 18);
90
137
  let raw;
91
138
  if (args.orderId) {
92
139
  // Update existing
93
- const { getMarketDetail } = await import("../services/marketService.js");
94
- const marketRes = await getMarketDetail(client, args.poolId, chainId);
95
- const market = marketRes.data ?? marketRes;
96
140
  const orderSnapshot = await findOrderSnapshot(client, address, chainId, args.orderId, args.poolId);
97
141
  const snapshotSize = readSnapshotText(orderSnapshot, ["size", "orderSize", "positionSize"]);
98
142
  const snapshotPrice = readSnapshotText(orderSnapshot, ["price", "orderPrice", "triggerPrice"]);
@@ -134,23 +178,50 @@ export const manageTpSlTool = {
134
178
  }
135
179
  else {
136
180
  // Create new
137
- if (direction === undefined || !args.leverage) {
181
+ if (!args.positionId)
182
+ throw new Error("positionId is required when creating new TP/SL.");
183
+ const positionSnapshot = await findPositionSnapshot(client, address, args.poolId, args.positionId);
184
+ let resolvedDirection = direction;
185
+ if (resolvedDirection === undefined && positionSnapshot?.direction !== undefined) {
186
+ resolvedDirection = normalizeDirectionInput(positionSnapshot.direction);
187
+ }
188
+ let resolvedLeverage = args.leverage;
189
+ if ((!resolvedLeverage || Number(resolvedLeverage) <= 0) && positionSnapshot) {
190
+ const leverageCandidate = Number(positionSnapshot?.userLeverage ??
191
+ positionSnapshot?.leverage ??
192
+ positionSnapshot?.positionLeverage);
193
+ if (Number.isFinite(leverageCandidate) && leverageCandidate > 0) {
194
+ resolvedLeverage = leverageCandidate;
195
+ }
196
+ }
197
+ if (resolvedDirection === undefined || !resolvedLeverage || Number(resolvedLeverage) <= 0) {
138
198
  throw new Error("direction and leverage are required when creating new TP/SL.");
139
199
  }
140
- if (!args.positionId) {
141
- throw new Error("positionId is required when creating new TP/SL.");
200
+ let tpSizeInput = isNonEmpty(args.tpSize) ? String(args.tpSize) : "";
201
+ let slSizeInput = isNonEmpty(args.slSize) ? String(args.slSize) : "";
202
+ const needsTpSize = isNonEmpty(args.tpPrice) && !tpSizeInput;
203
+ const needsSlSize = isNonEmpty(args.slPrice) && !slSizeInput;
204
+ if (needsTpSize || needsSlSize) {
205
+ const positionSizeRaw = resolvePositionSizeRaw(positionSnapshot, baseDecimals);
206
+ if (!positionSizeRaw) {
207
+ throw new Error("tpSize/slSize missing and unable to infer position size from live snapshot. Please provide tpSize/slSize explicitly.");
208
+ }
209
+ if (needsTpSize)
210
+ tpSizeInput = `raw:${positionSizeRaw}`;
211
+ if (needsSlSize)
212
+ slSizeInput = `raw:${positionSizeRaw}`;
142
213
  }
143
214
  raw = await setPositionTpSl(client, address, {
144
215
  poolId: args.poolId,
145
216
  positionId: args.positionId,
146
- direction,
147
- leverage: args.leverage,
148
- executionFeeToken: args.executionFeeToken,
217
+ direction: resolvedDirection,
218
+ leverage: Number(resolvedLeverage),
219
+ executionFeeToken: args.executionFeeToken || market.quoteToken,
149
220
  slippagePct,
150
221
  tpPrice: args.tpPrice,
151
- tpSize: args.tpSize,
222
+ tpSize: tpSizeInput || undefined,
152
223
  slPrice: args.slPrice,
153
- slSize: args.slSize
224
+ slSize: slSizeInput || undefined
154
225
  }, chainId);
155
226
  }
156
227
  const data = await finalizeMutationResult(raw, signer, "manage_tp_sl");
@@ -185,15 +185,17 @@ export const openPositionSimpleTool = {
185
185
  tradingFeeMeta = { source: "computed", assetClass, riskTier, feeRate: null, error: null };
186
186
  try {
187
187
  const feeRes = await client.utils.getUserTradingFeeRate(assetClass, riskTier, chainId);
188
- if (feeRes && Number(feeRes.code) === 0 && feeRes.data) {
189
- const rateRaw = postOnly ? feeRes.data.makerFeeRate : feeRes.data.takerFeeRate;
188
+ const hasData = feeRes && typeof feeRes === "object" && "data" in feeRes && feeRes.data;
189
+ if (feeRes && Number(feeRes.code) === 0 && hasData) {
190
+ const feeData = feeRes.data;
191
+ const rateRaw = postOnly ? feeData.makerFeeRate : feeData.takerFeeRate;
190
192
  tradingFeeMeta.feeRate = rateRaw;
191
193
  const rate = asBigint(String(rateRaw), "feeRate");
192
194
  const fee = (collateralRawBig * rate) / 1000000n;
193
195
  tradingFeeRaw = fee.toString();
194
196
  }
195
197
  else {
196
- tradingFeeMeta.error = feeRes?.message ?? "fee_rate_unavailable";
198
+ tradingFeeMeta.error = String((feeRes && (feeRes.message ?? feeRes.msg)) || "fee_rate_unavailable");
197
199
  }
198
200
  }
199
201
  catch (e) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@michaleffffff/mcp-trading-server",
3
- "version": "3.0.5",
3
+ "version": "3.0.7",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "myx-mcp": "dist/server.js"
@@ -20,7 +20,7 @@
20
20
  },
21
21
  "dependencies": {
22
22
  "@modelcontextprotocol/sdk": "^1.27.1",
23
- "@myx-trade/sdk": "^0.1.270",
23
+ "@myx-trade/sdk": "^1.0.2",
24
24
  "dotenv": "^17.3.1",
25
25
  "ethers": "^6.13.1",
26
26
  "zod": "^4.3.6"