@michaleffffff/mcp-trading-server 2.5.1 → 2.5.3

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.1" }, { capabilities: { tools: {}, resources: {}, prompts: {} } });
84
+ const server = new Server({ name: "myx-mcp-trading-server", version: "2.5.3" }, { 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.4.1 running (stdio, pure on-chain, prod ready)");
184
+ logger.info("🚀 MYX Trading MCP Server v2.5.3 running (stdio, pure on-chain, prod ready)");
185
185
  }
186
186
  main().catch((err) => {
187
187
  logger.error("Fatal Server Startup Error", err);
@@ -3,6 +3,7 @@ import { getChainId, getQuoteToken } from "../auth/resolveClient.js";
3
3
  import { ensureUnits } from "../utils/units.js";
4
4
  import { normalizeAddress } from "../utils/address.js";
5
5
  import { normalizeSlippagePct4dp } from "../utils/slippage.js";
6
+ import { finalizeMutationResult } from "../utils/mutationResult.js";
6
7
  function resolveDirection(direction) {
7
8
  if (direction !== 0 && direction !== 1) {
8
9
  throw new Error("direction must be 0 (LONG) or 1 (SHORT).");
@@ -58,6 +59,48 @@ export async function openPosition(client, address, args) {
58
59
  }
59
60
  const baseDecimals = poolData.baseDecimals || 18;
60
61
  const quoteDecimals = poolData.quoteDecimals || 6;
62
+ const collateralRaw = ensureUnits(args.collateralAmount, quoteDecimals, "collateralAmount");
63
+ const sizeRaw = ensureUnits(args.size, baseDecimals, "size");
64
+ const priceRaw = ensureUnits(args.price, 30, "price");
65
+ const tradingFeeRaw = ensureUnits(args.tradingFee, quoteDecimals, "tradingFee");
66
+ // --- Auto-Deposit Logic (Strict) ---
67
+ console.log(`[tradeService] Checking marginBalance for ${address} in pool ${args.poolId}...`);
68
+ const marginInfo = await client.account.getAccountInfo(chainId, address, args.poolId);
69
+ let marginBalanceRaw = BigInt(0);
70
+ let walletBalanceRaw = BigInt(0); // Using availableMargin as wallet balance per user
71
+ if (marginInfo?.code === 0) {
72
+ if (Array.isArray(marginInfo.data)) {
73
+ marginBalanceRaw = BigInt(marginInfo.data[0] || "0");
74
+ walletBalanceRaw = BigInt(marginInfo.data[1] || "0");
75
+ }
76
+ else {
77
+ marginBalanceRaw = BigInt(marginInfo.data?.marginBalance || "0");
78
+ walletBalanceRaw = BigInt(marginInfo.data?.availableMargin || "0");
79
+ }
80
+ }
81
+ const requiredRaw = BigInt(collateralRaw);
82
+ if (marginBalanceRaw < requiredRaw) {
83
+ const neededRaw = requiredRaw - marginBalanceRaw;
84
+ console.log(`[tradeService] marginBalance (${marginBalanceRaw.toString()}) < Required (${requiredRaw.toString()}). Need to deposit: ${neededRaw.toString()}`);
85
+ if (walletBalanceRaw < neededRaw) {
86
+ // Also check real wallet balance just in case user's availableMargin is truly empty
87
+ const realWalletRes = await client.account.getWalletQuoteTokenBalance(chainId, address);
88
+ const realWalletRaw = BigInt(realWalletRes?.data || "0");
89
+ if (realWalletRaw < neededRaw) {
90
+ throw new Error(`Insufficient funds: marginBalance (${marginBalanceRaw.toString()}) + availableMargin (as wallet: ${walletBalanceRaw.toString()}) + real wallet (${realWalletRaw.toString()}) is less than required collateral (${requiredRaw.toString()}).`);
91
+ }
92
+ walletBalanceRaw = realWalletRaw;
93
+ }
94
+ console.log(`[tradeService] Depositing ${neededRaw.toString()} ${poolData.quoteSymbol} from wallet (availableMargin)...`);
95
+ const depositRaw = await client.account.deposit({
96
+ amount: neededRaw.toString(),
97
+ tokenAddress: poolData.quoteToken,
98
+ chainId
99
+ });
100
+ const depositResult = await finalizeMutationResult(depositRaw, client.signer || (client.provider ? { provider: client.provider } : null), "auto_deposit");
101
+ console.log(`[tradeService] Auto-deposit confirmed in tx: ${depositResult.confirmation?.txHash}`);
102
+ }
103
+ // --- End Auto-Deposit Logic ---
61
104
  const orderParams = {
62
105
  chainId,
63
106
  address,
@@ -66,9 +109,9 @@ export async function openPosition(client, address, args) {
66
109
  orderType: args.orderType,
67
110
  triggerType: resolveTriggerType(args.orderType, args.direction, args.triggerType),
68
111
  direction: dir,
69
- collateralAmount: ensureUnits(args.collateralAmount, quoteDecimals, "collateralAmount"),
70
- size: ensureUnits(args.size, baseDecimals, "size"),
71
- price: ensureUnits(args.price, 30, "price"),
112
+ collateralAmount: collateralRaw,
113
+ size: sizeRaw,
114
+ price: priceRaw,
72
115
  timeInForce: args.timeInForce,
73
116
  postOnly: args.postOnly,
74
117
  slippagePct: normalizeSlippagePct4dp(args.slippagePct),
@@ -83,7 +126,6 @@ export async function openPosition(client, address, args) {
83
126
  orderParams.slSize = ensureUnits(args.slSize, baseDecimals, "slSize");
84
127
  if (args.slPrice)
85
128
  orderParams.slPrice = ensureUnits(args.slPrice, 30, "slPrice");
86
- const tradingFeeRaw = ensureUnits(args.tradingFee, quoteDecimals, "tradingFee");
87
129
  return client.order.createIncreaseOrder(orderParams, tradingFeeRaw, args.marketId);
88
130
  }
89
131
  /**
@@ -3,6 +3,7 @@ import { resolveClient } from "../auth/resolveClient.js";
3
3
  import { closePosition as closePos } from "../services/tradeService.js";
4
4
  import { finalizeMutationResult } from "../utils/mutationResult.js";
5
5
  import { isValidSlippagePct4dp, SLIPPAGE_PCT_4DP_DESC } from "../utils/slippage.js";
6
+ import { verifyTradeOutcome } from "../utils/verification.js";
6
7
  export const closePositionTool = {
7
8
  name: "close_position",
8
9
  description: "Create a decrease order using SDK-native parameters.",
@@ -28,7 +29,13 @@ export const closePositionTool = {
28
29
  const { client, address, signer } = await resolveClient();
29
30
  const raw = await closePos(client, address, args);
30
31
  const data = await finalizeMutationResult(raw, signer, "close_position");
31
- return { content: [{ type: "text", text: JSON.stringify({ status: "success", data }, (_, v) => typeof v === 'bigint' ? v.toString() : v, 2) }] };
32
+ const txHash = data.confirmation?.txHash;
33
+ let verification = null;
34
+ if (txHash) {
35
+ verification = await verifyTradeOutcome(client, address, args.poolId, txHash);
36
+ }
37
+ const payload = { ...data, verification };
38
+ return { content: [{ type: "text", text: JSON.stringify({ status: "success", data: payload }, (_, v) => typeof v === 'bigint' ? v.toString() : v, 2) }] };
32
39
  }
33
40
  catch (error) {
34
41
  return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
@@ -3,77 +3,8 @@ import { resolveClient } from "../auth/resolveClient.js";
3
3
  import { openPosition } from "../services/tradeService.js";
4
4
  import { finalizeMutationResult } from "../utils/mutationResult.js";
5
5
  import { isValidSlippagePct4dp, SLIPPAGE_PCT_4DP_DESC } from "../utils/slippage.js";
6
+ import { verifyTradeOutcome } from "../utils/verification.js";
6
7
  const POSITION_ID_RE = /^$|^0x[0-9a-fA-F]{64}$/;
7
- function toList(input) {
8
- if (Array.isArray(input))
9
- return input;
10
- if (Array.isArray(input?.data))
11
- return input.data;
12
- return [];
13
- }
14
- function buildPositionFingerprint(rows, poolId) {
15
- const map = new Map();
16
- for (const row of rows) {
17
- if (String(row?.poolId ?? row?.pool_id ?? "") !== poolId)
18
- continue;
19
- const id = String(row?.positionId ?? row?.position_id ?? "");
20
- if (!id)
21
- continue;
22
- const fingerprint = JSON.stringify({
23
- size: String(row?.size ?? ""),
24
- collateralAmount: String(row?.collateralAmount ?? row?.collateral_amount ?? ""),
25
- txTime: Number(row?.txTime ?? row?.tx_time ?? 0),
26
- });
27
- map.set(id, fingerprint);
28
- }
29
- return map;
30
- }
31
- function buildOpenOrderSet(rows, poolId) {
32
- const set = new Set();
33
- for (const row of rows) {
34
- if (String(row?.poolId ?? row?.pool_id ?? "") !== poolId)
35
- continue;
36
- const id = row?.orderId ?? row?.id ?? row?.order_id;
37
- if (id !== undefined && id !== null)
38
- set.add(String(id));
39
- }
40
- return set;
41
- }
42
- async function snapshotTradeState(client, address, poolId) {
43
- const [ordersRes, positionsRes] = await Promise.all([
44
- client.order.getOrders(address).catch(() => null),
45
- client.position.listPositions(address).catch(() => null),
46
- ]);
47
- return {
48
- openOrders: buildOpenOrderSet(toList(ordersRes), poolId),
49
- positions: buildPositionFingerprint(toList(positionsRes), poolId),
50
- };
51
- }
52
- function hasTradeEffect(before, after) {
53
- for (const id of after.openOrders) {
54
- if (!before.openOrders.has(id))
55
- return true;
56
- }
57
- for (const [id, fp] of after.positions.entries()) {
58
- if (!before.positions.has(id))
59
- return true;
60
- if (before.positions.get(id) !== fp)
61
- return true;
62
- }
63
- return false;
64
- }
65
- async function waitForTradeEffect(client, address, poolId, before) {
66
- const maxAttempts = 8;
67
- const intervalMs = 1500;
68
- for (let i = 0; i < maxAttempts; i += 1) {
69
- const after = await snapshotTradeState(client, address, poolId);
70
- if (hasTradeEffect(before, after)) {
71
- return { detected: true, attempts: i + 1 };
72
- }
73
- await new Promise((resolve) => setTimeout(resolve, intervalMs));
74
- }
75
- return { detected: false, attempts: maxAttempts };
76
- }
77
8
  export const executeTradeTool = {
78
9
  name: "execute_trade",
79
10
  description: "Create an increase order using SDK-native parameters.",
@@ -85,9 +16,9 @@ export const executeTradeTool = {
85
16
  orderType: z.coerce.number().int().min(0).max(3).describe("OrderType enum value"),
86
17
  triggerType: z.coerce.number().int().min(0).max(2).describe("TriggerType enum value"),
87
18
  direction: z.coerce.number().pipe(z.union([z.literal(0), z.literal(1)])).describe("0 = LONG, 1 = SHORT"),
88
- collateralAmount: z.coerce.string().regex(/^\d+$/).describe("Collateral amount (raw units)"),
89
- size: z.coerce.string().regex(/^\d+$/).describe("Position size (raw units)"),
90
- price: z.coerce.string().regex(/^\d+$/).describe("Price (30-decimal raw units)"),
19
+ collateralAmount: z.coerce.string().describe("Collateral amount (raw or human-readable)"),
20
+ size: z.coerce.string().describe("Position size (raw or human-readable)"),
21
+ price: z.coerce.string().describe("Price (raw or human-readable, 30 decimals)"),
91
22
  timeInForce: z.coerce.number().int().describe("TimeInForce enum value"),
92
23
  postOnly: z.coerce.boolean().describe("Post-only flag"),
93
24
  slippagePct: z.coerce.string().refine(isValidSlippagePct4dp, {
@@ -95,25 +26,25 @@ export const executeTradeTool = {
95
26
  }).describe(SLIPPAGE_PCT_4DP_DESC),
96
27
  executionFeeToken: z.string().describe("Execution fee token address"),
97
28
  leverage: z.coerce.number().describe("Leverage"),
98
- tpSize: z.coerce.string().regex(/^\d+$/).optional().describe("TP size raw units"),
99
- tpPrice: z.coerce.string().regex(/^\d+$/).optional().describe("TP price raw units (30 decimals)"),
100
- slSize: z.coerce.string().regex(/^\d+$/).optional().describe("SL size raw units"),
101
- slPrice: z.coerce.string().regex(/^\d+$/).optional().describe("SL price raw units (30 decimals)"),
102
- tradingFee: z.coerce.string().regex(/^\d+$/).describe("Trading fee raw units"),
29
+ tpSize: z.coerce.string().optional().describe("TP size (raw or human-readable)"),
30
+ tpPrice: z.coerce.string().optional().describe("TP price (raw or human-readable)"),
31
+ slSize: z.coerce.string().optional().describe("SL size (raw or human-readable)"),
32
+ slPrice: z.coerce.string().optional().describe("SL price (raw or human-readable)"),
33
+ tradingFee: z.coerce.string().describe("Trading fee (raw units)"),
103
34
  marketId: z.string().describe("Market ID"),
104
35
  },
105
36
  handler: async (args) => {
106
37
  try {
107
38
  const { client, address, signer } = await resolveClient();
108
- const before = await snapshotTradeState(client, address, args.poolId);
109
39
  const raw = await openPosition(client, address, args);
110
40
  const data = await finalizeMutationResult(raw, signer, "execute_trade");
111
- const effect = await waitForTradeEffect(client, address, args.poolId, before);
112
- if (!effect.detected) {
113
- throw new Error(`execute_trade tx confirmed but no order/position change detected in ${effect.attempts} checks.`);
41
+ const txHash = data.confirmation?.txHash;
42
+ let verification = null;
43
+ if (txHash) {
44
+ verification = await verifyTradeOutcome(client, address, args.poolId, txHash);
114
45
  }
115
- const payload = { ...data, effect };
116
- return { content: [{ type: "text", text: JSON.stringify({ status: "success", data: payload }, (k, v) => typeof v === 'bigint' ? v.toString() : v) }] };
46
+ const payload = { ...data, verification };
47
+ return { content: [{ type: "text", text: JSON.stringify({ status: "success", data: payload }, (k, v) => typeof v === 'bigint' ? v.toString() : v, 2) }] };
117
48
  }
118
49
  catch (error) {
119
50
  return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
@@ -0,0 +1,44 @@
1
+ import { getChainId } from "../auth/resolveClient.js";
2
+ /**
3
+ * 等待后端索引并验证交易结果
4
+ */
5
+ export async function verifyTradeOutcome(client, address, poolId, txHash) {
6
+ const chainId = getChainId();
7
+ // 给后端索引一定的缓冲时间
8
+ const maxAttempts = 5;
9
+ const intervalMs = 1000;
10
+ let matchedOrder = null;
11
+ for (let i = 0; i < maxAttempts; i++) {
12
+ try {
13
+ // 查询历史订单
14
+ const historyRes = await client.order.getOrderHistory({
15
+ chainId,
16
+ poolId,
17
+ limit: 10
18
+ }, address);
19
+ const history = historyRes?.data || historyRes?.data?.data || [];
20
+ matchedOrder = history.find((o) => String(o.orderHash).toLowerCase() === txHash.toLowerCase() ||
21
+ String(o.txHash).toLowerCase() === txHash.toLowerCase());
22
+ if (matchedOrder)
23
+ break;
24
+ }
25
+ catch (e) {
26
+ console.warn(`[verifyTradeOutcome] Attempt ${i + 1} failed:`, e);
27
+ }
28
+ await new Promise(resolve => setTimeout(resolve, intervalMs));
29
+ }
30
+ // 查询当前持仓
31
+ let positions = [];
32
+ try {
33
+ const posRes = await client.position.listPositions(address);
34
+ positions = (posRes?.data || []).filter((p) => String(p.poolId) === poolId);
35
+ }
36
+ catch (e) {
37
+ console.warn(`[verifyTradeOutcome] Failed to fetch positions:`, e);
38
+ }
39
+ return {
40
+ order: matchedOrder,
41
+ positions: positions,
42
+ verified: !!matchedOrder
43
+ };
44
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@michaleffffff/mcp-trading-server",
3
- "version": "2.5.1",
3
+ "version": "2.5.3",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "myx-mcp": "dist/server.js"