@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.
|
|
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.
|
|
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:
|
|
70
|
-
size:
|
|
71
|
-
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
|
-
|
|
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().
|
|
89
|
-
size: z.coerce.string().
|
|
90
|
-
price: z.coerce.string().
|
|
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().
|
|
99
|
-
tpPrice: z.coerce.string().
|
|
100
|
-
slSize: z.coerce.string().
|
|
101
|
-
slPrice: z.coerce.string().
|
|
102
|
-
tradingFee: z.coerce.string().
|
|
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
|
|
112
|
-
|
|
113
|
-
|
|
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,
|
|
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
|
+
}
|