@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 +38 -6
- package/dist/server.js +2 -2
- package/dist/services/marketService.js +3 -2
- package/dist/services/tradeService.js +5 -0
- package/dist/tools/index.js +1 -0
- package/dist/tools/openPositionSimple.js +345 -0
- package/dist/tools/setTpSl.js +4 -4
- package/dist/tools/updateOrderTpSl.js +6 -6
- package/dist/utils/units.js +25 -0
- package/package.json +2 -2
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 `
|
|
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
|
-
"
|
|
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
|
|
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
|
|
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 `
|
|
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.
|
|
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.
|
|
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
|
|
5
|
+
const tickerRes = await client.markets.getTickerList({
|
|
6
6
|
chainId,
|
|
7
7
|
poolIds: [poolId],
|
|
8
8
|
});
|
|
9
|
-
|
|
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) {
|
package/dist/tools/index.js
CHANGED
|
@@ -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
|
+
};
|
package/dist/tools/setTpSl.js
CHANGED
|
@@ -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().
|
|
21
|
-
tpSize: z.coerce.string().
|
|
22
|
-
slPrice: z.coerce.string().
|
|
23
|
-
slSize: z.coerce.string().
|
|
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().
|
|
12
|
-
price: z.string().
|
|
13
|
-
tpPrice: z.string().
|
|
14
|
-
tpSize: z.string().
|
|
15
|
-
slPrice: z.string().
|
|
16
|
-
slSize: z.string().
|
|
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"),
|
package/dist/utils/units.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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"
|