@michaleffffff/mcp-trading-server 3.0.15 → 3.0.20

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,43 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.0.20 - 2026-03-19
4
+
5
+ ### Fixed
6
+ - Hardened `get_pool_metadata` funding-rate formatting:
7
+ - `fundingInfo.nextFundingRate` now preserves readable `%/秒` and `%/天` output for negative rates too.
8
+ - Regression coverage now asserts both numeric funding-rate views and display strings.
9
+
10
+ ## 3.0.19 - 2026-03-19
11
+
12
+ ### Fixed
13
+ - Refined `fundingInfo.nextFundingRate` formatting in `get_pool_metadata`:
14
+ - Displays percent per second as `%/秒`
15
+ - Displays derived percent per day as `%/天`
16
+ - Keeps raw integer and comma-separated views for audit/debug use
17
+
18
+ ## 3.0.18 - 2026-03-19
19
+
20
+ ### Fixed
21
+ - Further improved `fundingInfo` / `ioTracker` readability in `get_pool_metadata`:
22
+ - `fundingInfo.nextEpochTime` now includes UTC timestamp and seconds-until-next-epoch.
23
+ - `fundingInfo.nextFundingRate` and `lastFundingFeeTracker` now include comma-separated raw views.
24
+ - `ioTracker` now includes derived notional-at-entry views based on `poolEntryPrice`.
25
+
26
+ ## 3.0.17 - 2026-03-19
27
+
28
+ ### Fixed
29
+ - Improved precision handling for liquidity and pool read tools:
30
+ - `get_pool_metadata` now adds `poolInfoFormatted` for exchange rate, LP token price, LP supply, debt, collateral, reserves, and open interest.
31
+ - `get_lp_price` now returns both raw and human-readable formatted values.
32
+ - Added regression coverage to ensure formatted precision fields are present.
33
+
34
+ ## 3.0.16 - 2026-03-18
35
+
36
+ ### Fixed
37
+ - Improved MCP-side validation UX:
38
+ - `executionFeeToken` now fails early with a clear `INVALID_PARAM` when callers pass the zero address, and points users to the real pool `quoteToken`.
39
+ - `open_position_simple` no longer returns a generic numeric parse error when `collateralAmount` is omitted; it now explains that `collateralAmount` is still required and suggests an approximate value from `size`, `price`, and `leverage`.
40
+
3
41
  ## 3.0.15 - 2026-03-18
4
42
 
5
43
  ### Fixed
package/README.md CHANGED
@@ -6,7 +6,7 @@ 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.15**
9
+ - **Current release: 3.0.20**
10
10
  - **SDK baseline**: `@myx-trade/sdk@^1.0.2` compatibility completed.
11
11
  - **Refinement**: Consolidated 40+ specialized tools into ~26 high-level unified tools.
12
12
  - **Improved UX**: Enhanced AI parameter parsing, automated unit conversion, and structured error reporting.
@@ -20,6 +20,7 @@ A production-ready MCP (Model Context Protocol) server for deep integration with
20
20
  * **AI-First Design**: Automated Pool ID resolution and flexible unit handling (`human:` vs `raw:`).
21
21
  * **Deep Liquidity Support**: Tools for both traders and liquidity providers.
22
22
  * **Production Ready**: Robust error handling with actionable hints for LLMs.
23
+ * **Precision-Aware Reads**: Pool and LP read tools expose human-readable formatted values alongside raw on-chain integers.
23
24
  * **Compliant**: Full Model Context Protocol (MCP) support.
24
25
 
25
26
  ---
@@ -109,6 +110,7 @@ Use these conventions when generating tool arguments:
109
110
  - `orderType`: `MARKET` / `LIMIT` / `STOP` / `CONDITIONAL`
110
111
  - `timeInForce`: SDK `v1.0.2` currently supports `IOC` only, so use `0` or `"IOC"`
111
112
  - `size`: base token quantity, not USD notional; expected order value is usually `collateralAmount * leverage`
113
+ - `executionFeeToken`: must be a real token address; zero address is rejected. Use the pool `quoteToken`
112
114
  - Human units: `"100"` means 100 USDC or 100 token units depending on field
113
115
  - Raw units: `"raw:1000000"` means exact on-chain integer units
114
116
 
package/TOOL_EXAMPLES.md CHANGED
@@ -1,4 +1,4 @@
1
- # MYX MCP Tool Examples Handbook (v3.0.15)
1
+ # MYX MCP Tool Examples Handbook (v3.0.20)
2
2
 
3
3
  This guide provides practical MCP payload examples for the current unified toolset.
4
4
  All examples use the MCP format:
@@ -74,6 +74,7 @@ Recommended high-level entry tool.
74
74
 
75
75
  `marketId` is optional on `open_position_simple`. If supplied, it is validated against the market resolved from `poolId` or `keyword`.
76
76
  `size` is always the base-asset quantity, not the USD notional. For example, a 500 USD order at price 1200 implies `size ≈ 0.416666...`.
77
+ `collateralAmount` remains required on `open_position_simple`; if omitted, MCP now returns an actionable suggestion instead of a generic parse error.
77
78
 
78
79
  Raw-units example:
79
80
 
@@ -111,6 +112,7 @@ Low-level increase-order tool when you want full control.
111
112
  ```
112
113
 
113
114
  `timeInForce` should be `0` (or `"IOC"` in string form) for SDK `v1.0.2`.
115
+ `executionFeeToken` must be a real token address; do not pass the zero address. Use the pool `quoteToken`.
114
116
 
115
117
  ### `close_position`
116
118
  Close or reduce a position. Use `ALL` for a full close.
@@ -231,6 +233,8 @@ Unified pool detail, config, and liquidity info.
231
233
  }
232
234
  ```
233
235
 
236
+ `get_pool_metadata` returns raw values in `poolInfo` and precision-safe human-readable values in `poolInfoFormatted`, including readable funding epoch timestamps, funding-rate `%/秒` and `%/天`, and IO notional-at-entry views.
237
+
234
238
  ### `get_kline`
235
239
  Read chart data. Use `limit: 1` for the latest bar.
236
240
 
@@ -293,6 +297,8 @@ Read LP NAV price for BASE or QUOTE side.
293
297
  }
294
298
  ```
295
299
 
300
+ `get_lp_price` returns both `raw` and `formatted` NAV price values.
301
+
296
302
  ### `get_my_lp_holdings`
297
303
  Read current LP balances across pools.
298
304
 
package/dist/server.js CHANGED
@@ -39,7 +39,7 @@ function safeJsonStringify(value) {
39
39
  }
40
40
  function inferToolErrorCode(message) {
41
41
  const lower = message.toLowerCase();
42
- if (lower.includes("required") || lower.includes("invalid") || lower.includes("must be") || lower.includes("unexpected") || lower.includes("unrecognized")) {
42
+ if (lower.includes("required") || lower.includes("invalid") || lower.includes("must be") || lower.includes("unexpected") || lower.includes("unrecognized") || lower.includes("zero address")) {
43
43
  return "INVALID_PARAM";
44
44
  }
45
45
  if (lower.includes("insufficient") && (lower.includes("allowance") || lower.includes("approval"))) {
@@ -461,7 +461,7 @@ function zodSchemaToJsonSchema(zodSchema) {
461
461
  };
462
462
  }
463
463
  // ─── MCP Server ───
464
- const server = new Server({ name: "myx-mcp-trading-server", version: "3.0.15" }, { capabilities: { tools: {}, resources: {}, prompts: {} } });
464
+ const server = new Server({ name: "myx-mcp-trading-server", version: "3.0.20" }, { capabilities: { tools: {}, resources: {}, prompts: {} } });
465
465
  // List tools
466
466
  server.setRequestHandler(ListToolsRequestSchema, async () => {
467
467
  return {
@@ -582,7 +582,7 @@ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
582
582
  async function main() {
583
583
  const transport = new StdioServerTransport();
584
584
  await server.connect(transport);
585
- logger.info("🚀 MYX Trading MCP Server v3.0.15 running (stdio, pure on-chain, prod ready)");
585
+ logger.info("🚀 MYX Trading MCP Server v3.0.20 running (stdio, pure on-chain, prod ready)");
586
586
  }
587
587
  main().catch((err) => {
588
588
  logger.error("Fatal Server Startup Error", err);
@@ -7,6 +7,7 @@ import { verifyTradeOutcome } from "../utils/verification.js";
7
7
  import { mapDirection, mapOrderType, mapTriggerType } from "../utils/mappings.js";
8
8
  import { extractErrorMessage } from "../utils/errorMessage.js";
9
9
  import { parseUserUnits } from "../utils/units.js";
10
+ import { isZeroAddress } from "../utils/address.js";
10
11
  const POSITION_ID_RE = /^$|^0x[0-9a-fA-F]{64}$/;
11
12
  const ZERO_POSITION_ID_RE = /^0x0{64}$/i;
12
13
  export const executeTradeTool = {
@@ -61,6 +62,11 @@ export const executeTradeTool = {
61
62
  const mappedOrderType = mapOrderType(args.orderType);
62
63
  const mappedTriggerType = args.triggerType !== undefined ? mapTriggerType(args.triggerType) : undefined;
63
64
  const slippagePctNormalized = normalizeSlippagePct4dp(args.slippagePct);
65
+ if (args.executionFeeToken !== undefined && args.executionFeeToken !== null && String(args.executionFeeToken).trim() !== "") {
66
+ if (isZeroAddress(args.executionFeeToken)) {
67
+ throw new Error(`executionFeeToken cannot be zero address. Use the pool quoteToken address ${poolData.quoteToken}.`);
68
+ }
69
+ }
64
70
  const collateralRaw = parseUserUnits(args.collateralAmount, quoteDecimals, "collateralAmount");
65
71
  const sizeRaw = parseUserUnits(args.size, baseDecimals, "size");
66
72
  const priceRaw = parseUserUnits(args.price, 30, "price");
@@ -1,10 +1,12 @@
1
1
  import { z } from "zod";
2
+ import { formatUnits } from "ethers";
2
3
  import { resolveClient, getChainId } from "../auth/resolveClient.js";
3
4
  import { getMarketDetail, resolvePool } from "../services/marketService.js";
4
5
  import { getPoolInfo, getLiquidityInfo } from "../services/poolService.js";
5
6
  import { extractErrorMessage } from "../utils/errorMessage.js";
6
7
  import { parseUserPrice30 } from "../utils/units.js";
7
8
  const INTEGER_RE = /^\d+$/;
9
+ const SIGNED_INTEGER_RE = /^-?\d+$/;
8
10
  const DECIMAL_RE = /^\d+(\.\d+)?$/;
9
11
  function normalizeMarketPrice30Input(value) {
10
12
  const text = String(value ?? "").trim();
@@ -29,6 +31,180 @@ function compactWarning(scope, err) {
29
31
  }
30
32
  return `${scope}: ${flat}`;
31
33
  }
34
+ function formatRawValue(value, decimals) {
35
+ const text = String(value ?? "").trim();
36
+ if (!INTEGER_RE.test(text))
37
+ return null;
38
+ try {
39
+ return formatUnits(text, decimals);
40
+ }
41
+ catch {
42
+ return null;
43
+ }
44
+ }
45
+ function buildFormattedAmount(value, decimals, symbol) {
46
+ const raw = String(value ?? "");
47
+ const formatted = formatRawValue(value, decimals);
48
+ return {
49
+ raw,
50
+ decimals,
51
+ formatted,
52
+ display: formatted && symbol ? `${formatted} ${symbol}` : formatted,
53
+ symbol: symbol ?? null,
54
+ };
55
+ }
56
+ function buildFormattedPrice30(value, quoteSymbol) {
57
+ const raw = String(value ?? "");
58
+ const formatted = formatRawValue(value, 30);
59
+ return {
60
+ raw,
61
+ decimals: 30,
62
+ formatted,
63
+ display: formatted && quoteSymbol ? `${formatted} ${quoteSymbol}` : formatted,
64
+ symbol: quoteSymbol ?? null,
65
+ };
66
+ }
67
+ function buildFormattedRatio18(value) {
68
+ const raw = String(value ?? "");
69
+ const formatted = formatRawValue(value, 18);
70
+ return {
71
+ raw,
72
+ decimals: 18,
73
+ formatted,
74
+ display: formatted,
75
+ };
76
+ }
77
+ function buildRawScalar(value) {
78
+ const raw = String(value ?? "").trim();
79
+ if (!SIGNED_INTEGER_RE.test(raw)) {
80
+ return { raw, withCommas: raw || null };
81
+ }
82
+ try {
83
+ return {
84
+ raw,
85
+ withCommas: BigInt(raw).toLocaleString("en-US"),
86
+ };
87
+ }
88
+ catch {
89
+ return { raw, withCommas: raw };
90
+ }
91
+ }
92
+ function formatScaledIntegerString(raw, decimals) {
93
+ if (!SIGNED_INTEGER_RE.test(raw))
94
+ return null;
95
+ const negative = raw.startsWith("-");
96
+ const digits = negative ? raw.slice(1) : raw;
97
+ const padded = digits.padStart(decimals + 1, "0");
98
+ const intPart = padded.slice(0, -decimals) || "0";
99
+ const fracPart = padded.slice(-decimals).replace(/0+$/, "");
100
+ const normalized = fracPart ? `${intPart}.${fracPart}` : intPart;
101
+ return negative ? `-${normalized}` : normalized;
102
+ }
103
+ function buildFundingRateInfo(value) {
104
+ const base = buildRawScalar(value);
105
+ const raw = String(value ?? "").trim();
106
+ if (!SIGNED_INTEGER_RE.test(raw))
107
+ return base;
108
+ try {
109
+ const rawBig = BigInt(raw);
110
+ const perDayRaw = rawBig * 86400n;
111
+ const percentPerSecond = formatScaledIntegerString(raw, 12);
112
+ const percentPerDay = formatScaledIntegerString(perDayRaw.toString(), 12);
113
+ return {
114
+ ...base,
115
+ scale: "1e12 => percent",
116
+ percentPerSecond,
117
+ displayPerSecond: percentPerSecond ? `${percentPerSecond}%/秒` : null,
118
+ percentPerDay,
119
+ displayPerDay: percentPerDay ? `${percentPerDay}%/天` : null,
120
+ };
121
+ }
122
+ catch {
123
+ return base;
124
+ }
125
+ }
126
+ function buildTimestampInfo(value) {
127
+ const raw = String(value ?? "").trim();
128
+ const base = buildRawScalar(value);
129
+ if (!INTEGER_RE.test(raw)) {
130
+ return base;
131
+ }
132
+ const seconds = Number(raw);
133
+ if (!Number.isFinite(seconds) || seconds <= 0) {
134
+ return base;
135
+ }
136
+ const isoUtc = new Date(seconds * 1000).toISOString();
137
+ const secondsUntil = seconds - Math.floor(Date.now() / 1000);
138
+ return {
139
+ ...base,
140
+ isoUtc,
141
+ secondsUntil,
142
+ };
143
+ }
144
+ function computeQuoteNotionalDisplay(baseRaw, price30Raw, baseDecimals, quoteDecimals, quoteSymbol) {
145
+ const baseText = String(baseRaw ?? "").trim();
146
+ const priceText = String(price30Raw ?? "").trim();
147
+ if (!INTEGER_RE.test(baseText) || !INTEGER_RE.test(priceText))
148
+ return null;
149
+ try {
150
+ const notionalRaw = (BigInt(baseText) * BigInt(priceText) * (10n ** BigInt(quoteDecimals))) /
151
+ (10n ** BigInt(baseDecimals + 30));
152
+ return buildFormattedAmount(notionalRaw.toString(), quoteDecimals, quoteSymbol);
153
+ }
154
+ catch {
155
+ return null;
156
+ }
157
+ }
158
+ function formatPoolInfoSnapshot(poolInfo, marketDetail) {
159
+ if (!poolInfo || typeof poolInfo !== "object")
160
+ return null;
161
+ const baseSymbol = String(marketDetail?.baseSymbol ?? "BASE");
162
+ const quoteSymbol = String(marketDetail?.quoteSymbol ?? "QUOTE");
163
+ const baseDecimals = Number(marketDetail?.baseDecimals ?? 18);
164
+ const quoteDecimals = Number(marketDetail?.quoteDecimals ?? 6);
165
+ return {
166
+ quotePool: poolInfo.quotePool ? {
167
+ poolToken: poolInfo.quotePool.poolToken ?? null,
168
+ exchangeRate: buildFormattedRatio18(poolInfo.quotePool.exchangeRate),
169
+ poolTokenPrice: buildFormattedPrice30(poolInfo.quotePool.poolTokenPrice, quoteSymbol),
170
+ poolTokenSupply: buildFormattedAmount(poolInfo.quotePool.poolTokenSupply, 18, `m${quoteSymbol}.${baseSymbol}`),
171
+ totalDebt: buildFormattedAmount(poolInfo.quotePool.totalDebt, quoteDecimals, quoteSymbol),
172
+ baseCollateral: buildFormattedAmount(poolInfo.quotePool.baseCollateral, baseDecimals, baseSymbol),
173
+ } : null,
174
+ basePool: poolInfo.basePool ? {
175
+ poolToken: poolInfo.basePool.poolToken ?? null,
176
+ exchangeRate: buildFormattedRatio18(poolInfo.basePool.exchangeRate),
177
+ poolTokenPrice: buildFormattedPrice30(poolInfo.basePool.poolTokenPrice, quoteSymbol),
178
+ poolTokenSupply: buildFormattedAmount(poolInfo.basePool.poolTokenSupply, 18, `m${baseSymbol}.${quoteSymbol}`),
179
+ totalDebt: buildFormattedAmount(poolInfo.basePool.totalDebt, quoteDecimals, quoteSymbol),
180
+ baseCollateral: buildFormattedAmount(poolInfo.basePool.baseCollateral, baseDecimals, baseSymbol),
181
+ } : null,
182
+ reserveInfo: poolInfo.reserveInfo ? {
183
+ baseTotalAmount: buildFormattedAmount(poolInfo.reserveInfo.baseTotalAmount, baseDecimals, baseSymbol),
184
+ baseReservedAmount: buildFormattedAmount(poolInfo.reserveInfo.baseReservedAmount, baseDecimals, baseSymbol),
185
+ quoteTotalAmount: buildFormattedAmount(poolInfo.reserveInfo.quoteTotalAmount, quoteDecimals, quoteSymbol),
186
+ quoteReservedAmount: buildFormattedAmount(poolInfo.reserveInfo.quoteReservedAmount, quoteDecimals, quoteSymbol),
187
+ } : null,
188
+ fundingInfo: poolInfo.fundingInfo ? {
189
+ nextFundingRate: buildFundingRateInfo(poolInfo.fundingInfo.nextFundingRate),
190
+ lastFundingFeeTracker: buildRawScalar(poolInfo.fundingInfo.lastFundingFeeTracker),
191
+ nextEpochTime: buildTimestampInfo(poolInfo.fundingInfo.nextEpochTime),
192
+ } : null,
193
+ ioTracker: poolInfo.ioTracker ? {
194
+ tracker: buildFormattedAmount(poolInfo.ioTracker.tracker, baseDecimals, baseSymbol),
195
+ longSize: buildFormattedAmount(poolInfo.ioTracker.longSize, baseDecimals, baseSymbol),
196
+ shortSize: buildFormattedAmount(poolInfo.ioTracker.shortSize, baseDecimals, baseSymbol),
197
+ poolEntryPrice: buildFormattedPrice30(poolInfo.ioTracker.poolEntryPrice, quoteSymbol),
198
+ trackerNotionalAtEntry: computeQuoteNotionalDisplay(poolInfo.ioTracker.tracker, poolInfo.ioTracker.poolEntryPrice, baseDecimals, quoteDecimals, quoteSymbol),
199
+ longNotionalAtEntry: computeQuoteNotionalDisplay(poolInfo.ioTracker.longSize, poolInfo.ioTracker.poolEntryPrice, baseDecimals, quoteDecimals, quoteSymbol),
200
+ shortNotionalAtEntry: computeQuoteNotionalDisplay(poolInfo.ioTracker.shortSize, poolInfo.ioTracker.poolEntryPrice, baseDecimals, quoteDecimals, quoteSymbol),
201
+ } : null,
202
+ liquidityInfo: poolInfo.liquidityInfo ? {
203
+ windowCaps: buildFormattedAmount(poolInfo.liquidityInfo.windowCaps, quoteDecimals, quoteSymbol),
204
+ openInterest: buildFormattedAmount(poolInfo.liquidityInfo.openInterest, 18, quoteSymbol),
205
+ } : null,
206
+ };
207
+ }
32
208
  export const getPoolMetadataTool = {
33
209
  name: "get_pool_metadata",
34
210
  description: "[MARKET] Get comprehensive metadata for a pool (market detail, on-chain info, liquidity, and limits).",
@@ -57,6 +233,11 @@ export const getPoolMetadataTool = {
57
233
  // 2. Pool Info (Reserves, Utilization)
58
234
  try {
59
235
  results.poolInfo = await getPoolInfo(poolId, chainId, client);
236
+ const rawMarketDetail = results.marketDetail?.data ?? results.marketDetail;
237
+ const formatted = formatPoolInfoSnapshot(results.poolInfo, rawMarketDetail);
238
+ if (formatted) {
239
+ results.poolInfoFormatted = formatted;
240
+ }
60
241
  }
61
242
  catch (err) {
62
243
  errors.push(compactWarning("poolInfo", err));
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import { formatUnits } from "ethers";
2
3
  import { quoteDeposit, quoteWithdraw, baseDeposit, baseWithdraw, getLpPrice, } from "../services/poolService.js";
3
4
  import { resolveClient, getChainId } from "../auth/resolveClient.js";
4
5
  import { resolvePool } from "../services/marketService.js";
@@ -45,6 +46,20 @@ function resolveLpAssetNames(detail) {
45
46
  quoteLpAssetName,
46
47
  };
47
48
  }
49
+ function formatLpPricePayload(value, quoteSymbol) {
50
+ const raw = String(value ?? "").trim();
51
+ if (!/^\d+$/.test(raw)) {
52
+ return { raw, formatted: null, decimals: 30, symbol: quoteSymbol ?? null };
53
+ }
54
+ const formatted = formatUnits(raw, 30);
55
+ return {
56
+ raw,
57
+ formatted,
58
+ decimals: 30,
59
+ symbol: quoteSymbol ?? null,
60
+ display: quoteSymbol ? `${formatted} ${quoteSymbol}` : formatted,
61
+ };
62
+ }
48
63
  export const manageLiquidityTool = {
49
64
  name: "manage_liquidity",
50
65
  description: "[LIQUIDITY] Add or withdraw liquidity from a BASE or QUOTE pool. Success response includes LP naming metadata: base `mBASE.QUOTE`, quote `mQUOTE.BASE`, plus `operatedLpAssetName` based on poolType.",
@@ -135,8 +150,20 @@ export const getLpPriceTool = {
135
150
  },
136
151
  handler: async (args) => {
137
152
  try {
138
- const data = await getLpPrice(args.poolType, args.poolId, args.chainId);
139
- return { content: [{ type: "text", text: JSON.stringify({ status: "success", data }, (k, v) => typeof v === 'bigint' ? v.toString() : v, 2) }] };
153
+ const { client } = await resolveClient();
154
+ const chainId = args.chainId ?? getChainId();
155
+ const poolId = await resolvePool(client, args.poolId, undefined, chainId);
156
+ const detailRes = await client.markets.getMarketDetail({ chainId, poolId }).catch(() => null);
157
+ const detail = detailRes?.data || (detailRes?.marketId ? detailRes : null);
158
+ const quoteSymbol = String(detail?.quoteSymbol ?? "").trim() || null;
159
+ const rawPrice = await getLpPrice(args.poolType, poolId, chainId);
160
+ const payload = {
161
+ raw: String(rawPrice ?? ""),
162
+ formatted: formatLpPricePayload(rawPrice, quoteSymbol ?? undefined),
163
+ poolType: args.poolType,
164
+ poolId,
165
+ };
166
+ return { content: [{ type: "text", text: JSON.stringify({ status: "success", data: payload }, (k, v) => typeof v === 'bigint' ? v.toString() : v, 2) }] };
140
167
  }
141
168
  catch (error) {
142
169
  return { content: [{ type: "text", text: `Error: ${extractErrorMessage(error)}` }], isError: true };
@@ -1,9 +1,10 @@
1
1
  import { z } from "zod";
2
2
  import { OrderType } from "@myx-trade/sdk";
3
+ import { formatUnits } from "ethers";
3
4
  import { resolveClient, getChainId } from "../auth/resolveClient.js";
4
5
  import { resolvePool } from "../services/marketService.js";
5
6
  import { openPosition } from "../services/tradeService.js";
6
- import { normalizeAddress } from "../utils/address.js";
7
+ import { isZeroAddress, normalizeAddress } from "../utils/address.js";
7
8
  import { finalizeMutationResult } from "../utils/mutationResult.js";
8
9
  import { mapDirection, mapOrderType } from "../utils/mappings.js";
9
10
  import { normalizeSlippagePct4dp, SLIPPAGE_PCT_4DP_DESC } from "../utils/slippage.js";
@@ -43,6 +44,7 @@ export const openPositionSimpleTool = {
43
44
  direction: z.any().describe("0=LONG, 1=SHORT, or strings like 'BUY'/'SELL'/'LONG'/'SHORT'."),
44
45
  collateralAmount: z.coerce
45
46
  .string()
47
+ .optional()
46
48
  .describe("Collateral. e.g. '100' (quoted in USDC) or 'raw:100000000'."),
47
49
  leverage: z.coerce.number().int().positive().describe("Leverage (integer, e.g. 5, 10)."),
48
50
  orderType: z.union([z.string(), z.number()]).optional().describe("MARKET, LIMIT, STOP (default MARKET). Strings allowed."),
@@ -113,21 +115,14 @@ export const openPositionSimpleTool = {
113
115
  const defaultAssetClass = Number(poolLevelConfig?.levelConfig?.assetClass ?? 0);
114
116
  // 3) Parse & validate primary inputs
115
117
  const dir = mapDirection(args.direction);
116
- const collateralRaw = parseUserUnits(args.collateralAmount, quoteDecimals, "collateralAmount");
117
- const collateralRawBig = asBigint(collateralRaw, "collateralAmount");
118
- if (collateralRawBig <= 0n)
119
- throw new Error("collateralAmount must be > 0.");
120
- const maxTradeAmountHuman = String(process.env.MAX_TRADE_AMOUNT ?? "").trim();
121
- if (maxTradeAmountHuman) {
122
- const maxTradeRaw = parseUserUnits(maxTradeAmountHuman, quoteDecimals, "MAX_TRADE_AMOUNT");
123
- const maxTradeRawBig = asBigint(maxTradeRaw, "MAX_TRADE_AMOUNT");
124
- if (collateralRawBig > maxTradeRawBig) {
125
- throw new Error(`collateralAmount exceeds MAX_TRADE_AMOUNT (collateralRaw=${collateralRawBig.toString()} > maxRaw=${maxTradeRawBig.toString()}).`);
126
- }
127
- }
128
118
  const orderType = mapOrderType(args.orderType ?? 0);
129
119
  const postOnly = Boolean(args.postOnly ?? false);
130
120
  const slippagePct = normalizeSlippagePct4dp(args.slippagePct ?? "50");
121
+ if (args.executionFeeToken !== undefined && args.executionFeeToken !== null && String(args.executionFeeToken).trim() !== "") {
122
+ if (isZeroAddress(args.executionFeeToken)) {
123
+ throw new Error(`executionFeeToken cannot be zero address. Use the pool quoteToken address ${quoteToken}.`);
124
+ }
125
+ }
131
126
  const executionFeeToken = normalizeAddress(args.executionFeeToken || quoteToken, "executionFeeToken");
132
127
  // 4) Determine reference price (30 decimals)
133
128
  let price30;
@@ -157,6 +152,15 @@ export const openPositionSimpleTool = {
157
152
  const price30Big = asBigint(price30, "price");
158
153
  if (price30Big <= 0n)
159
154
  throw new Error("price must be > 0.");
155
+ const collateralInput = String(args.collateralAmount ?? "").trim();
156
+ let collateralRaw = "";
157
+ let collateralRawBig = 0n;
158
+ if (collateralInput) {
159
+ collateralRaw = parseUserUnits(collateralInput, quoteDecimals, "collateralAmount");
160
+ collateralRawBig = asBigint(collateralRaw, "collateralAmount");
161
+ if (collateralRawBig <= 0n)
162
+ throw new Error("collateralAmount must be > 0.");
163
+ }
160
164
  // 5) Compute or parse size (base raw units)
161
165
  let sizeRaw = "";
162
166
  let sizeMeta = { source: "computed" };
@@ -166,6 +170,9 @@ export const openPositionSimpleTool = {
166
170
  sizeMeta = { source: "user" };
167
171
  }
168
172
  else {
173
+ if (!collateralInput) {
174
+ throw new Error("Either collateralAmount or size is required for open_position_simple.");
175
+ }
169
176
  const notionalQuoteRaw = collateralRawBig * BigInt(args.leverage);
170
177
  const numerator = notionalQuoteRaw * pow10(30 + baseDecimals);
171
178
  const denominator = price30Big * pow10(quoteDecimals);
@@ -176,6 +183,22 @@ export const openPositionSimpleTool = {
176
183
  sizeRaw = computed.toString();
177
184
  sizeMeta = { source: "computed", notionalQuoteRaw: notionalQuoteRaw.toString() };
178
185
  }
186
+ const sizeRawBig = asBigint(sizeRaw, "size");
187
+ if (!collateralInput) {
188
+ const notionalQuoteRaw = (sizeRawBig * price30Big * pow10(quoteDecimals)) / pow10(baseDecimals + 30);
189
+ const suggestedCollateralRaw = notionalQuoteRaw / BigInt(args.leverage);
190
+ const suggestedCollateralHuman = formatUnits(suggestedCollateralRaw, quoteDecimals);
191
+ const notionalHuman = formatUnits(notionalQuoteRaw, quoteDecimals);
192
+ throw new Error(`collateralAmount is required for open_position_simple. Based on size=${userSize || formatUnits(sizeRawBig, baseDecimals)}, price=${priceMeta.human ?? formatUnits(price30Big, 30)}, leverage=${args.leverage}, implied order value is ≈${notionalHuman} quote and suggested collateralAmount is ≈${suggestedCollateralHuman}.`);
193
+ }
194
+ const maxTradeAmountHuman = String(process.env.MAX_TRADE_AMOUNT ?? "").trim();
195
+ if (maxTradeAmountHuman) {
196
+ const maxTradeRaw = parseUserUnits(maxTradeAmountHuman, quoteDecimals, "MAX_TRADE_AMOUNT");
197
+ const maxTradeRawBig = asBigint(maxTradeRaw, "MAX_TRADE_AMOUNT");
198
+ if (collateralRawBig > maxTradeRawBig) {
199
+ throw new Error(`collateralAmount exceeds MAX_TRADE_AMOUNT (collateralRaw=${collateralRawBig.toString()} > maxRaw=${maxTradeRawBig.toString()}).`);
200
+ }
201
+ }
179
202
  // 6) Compute tradingFee (quote raw units)
180
203
  let tradingFeeRaw = null;
181
204
  let tradingFeeMeta = { source: "computed" };
@@ -1,4 +1,5 @@
1
1
  import { getAddress } from "ethers";
2
+ export const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
2
3
  export function normalizeAddress(value, label = "address") {
3
4
  const raw = String(value || "").trim();
4
5
  if (!raw)
@@ -15,3 +16,14 @@ export function normalizeAddress(value, label = "address") {
15
16
  }
16
17
  }
17
18
  }
19
+ export function isZeroAddress(value) {
20
+ const raw = String(value ?? "").trim();
21
+ if (!raw)
22
+ return false;
23
+ try {
24
+ return getAddress(raw).toLowerCase() === ZERO_ADDRESS;
25
+ }
26
+ catch {
27
+ return raw.toLowerCase() === ZERO_ADDRESS;
28
+ }
29
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@michaleffffff/mcp-trading-server",
3
- "version": "3.0.15",
3
+ "version": "3.0.20",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "myx-mcp": "dist/server.js"