@michaleffffff/mcp-trading-server 3.1.0 → 3.1.2

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,10 +1,27 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.1.2 - 2026-03-23
4
+ ### Changed
5
+ - Added local preflight validation for opening `LIMIT` / `STOP` orders so `open_position_simple` and `execute_trade` now fail fast with clear guidance when:
6
+ - `triggerType` conflicts with the selected open-order semantics
7
+ - the target price is on the wrong side of the current oracle price for the chosen `LONG` / `SHORT` + `LIMIT` / `STOP` combination
8
+
9
+ ## 3.1.1 - 2026-03-23
10
+ ### Changed
11
+ - Upgraded the active SDK baseline to `@myx-trade/sdk@^1.0.4`.
12
+ - Synced operator-facing docs to the current baseline:
13
+ - `README.md` now references `@myx-trade/sdk@^1.0.4`.
14
+ - `mcp_config_guide.md` no longer refers to a separate Beta environment for LP / create-market flows.
15
+ - Current testnet live regression notes now align with the post-upgrade trade/account behavior.
16
+ - Deduplicated SDK compatibility helpers used by market lookup / `get_base_detail` without changing runtime behavior.
17
+
3
18
  ## 3.1.0 - 2026-03-20
4
19
  ### Changed
5
20
  - 发布 `3.1.0`,统一版本元数据到新的主次版本号。
6
21
  - 运行环境现在只区分测试环境与正式环境,不再走 beta host / beta 地址分支。
7
22
  - 修复测试环境下的市场发现链路,`list_pools` 恢复返回当前链池列表,`get_all_tickers` 在主接口不可用时继续走 fallback。
23
+ - `open_position_simple` 在用户提供 `size + price + leverage` 但未提供 `collateralAmount` 时,改为自动反推保证金;若用户传入的 `collateralAmount` 明显不足,则提前返回清晰错误。
24
+ - `execute_trade` / `close_position` 不再对外暴露 `timeInForce`;MCP 内部固定使用 `IOC (0)`,避免把一个不可配置字段误导成可配置参数。
8
25
 
9
26
  ## 3.0.31 - 2026-03-20
10
27
  ### Fixed
package/README.md CHANGED
@@ -7,10 +7,11 @@ A production-ready MCP (Model Context Protocol) server for deep integration with
7
7
  # Release Notes
8
8
 
9
9
  - **Current release: 3.1.0**
10
- - **SDK baseline**: `@myx-trade/sdk@^1.0.4-beta.4` compatibility completed.
10
+ - **SDK baseline**: `@myx-trade/sdk@^1.0.4` 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.
13
13
  - **Safety refresh**: Docs and prompt guidance now reflect explicit-price execution for `open_position_simple`, exact-approval defaults, notional-based fee checks, TP/SL semantic validation, and LP preview fail-close behavior.
14
+ - **Regression status**: Current trade/account compatibility was verified against the `1.0.4` baseline, including testnet live checks for `get_trade_flow`, `get_base_detail`, `account_deposit`, `account_withdraw`, `cancel_orders`, `manage_tp_sl`, and `close_position`.
14
15
  - **Breaking changes**: Many low-level tools (e.g., `get_market_price`, `get_oracle_price`, `get_open_orders`) have been merged into unified counterparts.
15
16
 
16
17
  ---
@@ -134,8 +135,9 @@ Use these conventions when generating tool arguments:
134
135
  - `status`: `OPEN` / `HISTORY` / `ALL` are canonical; lowercase is tolerated
135
136
  - `poolType`: `BASE` / `QUOTE` are canonical; lowercase is tolerated
136
137
  - `orderType`: `MARKET` / `LIMIT` / `STOP` / `CONDITIONAL`
137
- - `timeInForce`: SDK `v1.0.4-beta.4` currently supports `IOC` only, so use `0` or `"IOC"`
138
+ - Trading MCP tools now fix `timeInForce` internally to `IOC (0)`; do not pass this field
138
139
  - `size`: base token quantity, not USD notional; expected order value is usually `collateralAmount * leverage`
140
+ - `open_position_simple`: if `size + price + leverage` are provided, MCP can auto-compute `collateralAmount`; if `collateralAmount` is also provided, MCP validates it against the requested notional
139
141
  - `executionFeeToken`: must be a real token address; zero address is rejected. Use the pool `quoteToken`
140
142
  - `slippagePct`: trading tools use 4-decimal raw units where `100 = 1.00%` and `50 = 0.50%`
141
143
  - Human units: `"100"` means 100 USDC or 100 token units depending on field
package/TOOL_EXAMPLES.md CHANGED
@@ -81,7 +81,7 @@ Recommended high-level entry tool.
81
81
 
82
82
  `marketId` is optional on `open_position_simple`. If supplied, it is validated against the market resolved from `poolId` or `keyword`.
83
83
  `size` is always the base-asset quantity, not the USD notional. For example, a 500 USD order at price 1200 implies `size ≈ 0.416666...`.
84
- `collateralAmount` remains required on `open_position_simple`; if omitted, MCP now returns an actionable suggestion instead of a generic parse error.
84
+ If `size + price + leverage` are provided on `open_position_simple`, MCP can now auto-compute `collateralAmount`. If you also provide `collateralAmount`, MCP validates that it is sufficient for the requested position.
85
85
  Provide `price` explicitly for both `MARKET` and `LIMIT/STOP` on `open_position_simple`. MCP no longer auto-fills a fresh Oracle price for `MARKET`.
86
86
  Auto-computed `tradingFee` follows notional semantics rather than raw collateral-only estimation.
87
87
  `autoDeposit` is now a deprecated compatibility flag. MCP delegates increase-order funding deltas to the SDK during `createIncreaseOrder`.
@@ -116,13 +116,12 @@ Low-level increase-order tool when you want full control.
116
116
  "price": "2500",
117
117
  "size": "0.2",
118
118
  "collateralAmount": "100",
119
- "timeInForce": 0,
120
119
  "leverage": 5
121
120
  }
122
121
  }
123
122
  ```
124
123
 
125
- `timeInForce` should be `0` (or `"IOC"` in string form) for SDK `v1.0.4-beta.4`.
124
+ `timeInForce` is fixed internally to `IOC (0)` in MCP trading tools; do not pass this field.
126
125
  `executionFeeToken` must be a real token address; do not pass the zero address. Use the pool `quoteToken`.
127
126
  If `positionId` is supplied on increase flows, `direction` must remain consistent with the live position.
128
127
 
@@ -140,7 +139,6 @@ Close or reduce a position. Use `ALL` for a full close.
140
139
  "collateralAmount": "ALL",
141
140
  "size": "ALL",
142
141
  "price": "2200",
143
- "timeInForce": 0,
144
142
  "postOnly": false,
145
143
  "slippagePct": "50",
146
144
  "executionFeeToken": "0x...",
@@ -34,6 +34,7 @@ You are an expert crypto trader using the MYX Protocol. To ensure successful exe
34
34
  - **LP Strategy**: Use \`get_my_lp_holdings\` to monitor liquidity positions. Naming follows \`mBASE.QUOTE\` (e.g., \`mBTC.USDC\`).
35
35
  - **Enum Tolerance**: The server tolerates common lowercase or alias inputs such as \`open\`, \`base\`, \`buy\`, and \`add\`, but canonical forms are still preferred in documentation.
36
36
  - **Execution Price**: \`open_position_simple\` no longer auto-fills a fresh Oracle price for \`MARKET\`; provide \`price\` explicitly when you want MCP to compute size / fee previews.
37
+ - **Collateral Auto-Compute**: If you provide \`size + price + leverage\` to \`open_position_simple\`, MCP can infer \`collateralAmount\` automatically. If you provide \`collateralAmount\` too, MCP validates that it is sufficient for the intended notional.
37
38
  - **Funding Delta Ownership**: MCP no longer performs its own increase-order margin/deposit reconciliation. SDK \`createIncreaseOrder\` owns deposit-delta handling for new increase orders.
38
39
  - **Pre-check Diagnostics**: \`check_account_ready\` now reports SDK \`availableMarginBalance\` first. If that read degrades, inspect \`summary.degraded\` and \`diagnostics.availableMarginError\` before trusting fallback account fields.
39
40
  - **Approval Safety**: Local fallback flows prefer exact approval sizing. Do not assume unlimited approvals are necessary.
@@ -1,7 +1,7 @@
1
- import { getChainId } from "../auth/resolveClient.js";
1
+ import { getChainId, getQuoteToken } from "../auth/resolveClient.js";
2
2
  export async function getBalances(client, address, chainIdOverride) {
3
3
  const chainId = chainIdOverride ?? getChainId();
4
- return client.account.getWalletQuoteTokenBalance(chainId, address);
4
+ return client.account.getWalletQuoteTokenBalance({ chainId, address, tokenAddress: getQuoteToken() });
5
5
  }
6
6
  export async function getMarginBalance(client, address, poolId, chainIdOverride) {
7
7
  const chainId = chainIdOverride ?? getChainId();
@@ -69,10 +69,27 @@ function matchesKeyword(row, keywordUpper) {
69
69
  .join("|");
70
70
  return haystack.includes(keywordUpper);
71
71
  }
72
+ export async function searchMarketCompat(client, params) {
73
+ const normalizedKeyword = String(params.keyword ?? "").trim();
74
+ const attempts = [
75
+ { chainId: params.chainId, keyword: normalizedKeyword, limit: params.limit },
76
+ { chainId: params.chainId, searchKey: normalizedKeyword },
77
+ ];
78
+ let lastError = null;
79
+ for (const attempt of attempts) {
80
+ try {
81
+ return await client.markets.searchMarket(attempt);
82
+ }
83
+ catch (error) {
84
+ lastError = error;
85
+ }
86
+ }
87
+ throw lastError ?? new Error("searchMarket failed");
88
+ }
72
89
  async function fetchApiMarketRows(client, chainId) {
73
90
  // Preferred path: documented markets.searchMarket
74
91
  try {
75
- const searchRes = await client.markets.searchMarket({ chainId, keyword: "", limit: 2000 });
92
+ const searchRes = await searchMarketCompat(client, { chainId, keyword: "", limit: 2000 });
76
93
  const searchRows = extractMarketRows(searchRes, chainId).filter((row) => normalizePoolId(row) && matchesChainId(row, chainId));
77
94
  if (searchRows.length > 0)
78
95
  return searchRows;
@@ -156,7 +173,11 @@ export async function searchMarket(client, keyword, limit = 1000, chainIdOverrid
156
173
  const requestedLimit = Number.isFinite(limit) && limit > 0 ? Math.floor(limit) : 1000;
157
174
  let dataList = [];
158
175
  try {
159
- const searchRes = await client.markets.searchMarket({ chainId, keyword: normalizedKeyword, limit: requestedLimit });
176
+ const searchRes = await searchMarketCompat(client, {
177
+ chainId,
178
+ keyword: normalizedKeyword,
179
+ limit: requestedLimit,
180
+ });
160
181
  dataList = extractMarketRows(searchRes, chainId);
161
182
  }
162
183
  catch {
@@ -235,7 +256,7 @@ export async function getMarketDetail(client, poolId, chainIdOverride) {
235
256
  export async function getPoolList(client, chainIdOverride) {
236
257
  const chainId = chainIdOverride ?? getChainId();
237
258
  try {
238
- const searchRes = await client.markets.searchMarket({ chainId, keyword: "", limit: 2000 });
259
+ const searchRes = await searchMarketCompat(client, { chainId, keyword: "", limit: 2000 });
239
260
  const rows = extractMarketRows(searchRes, chainId).filter((row) => normalizePoolId(row) && matchesChainId(row, chainId));
240
261
  if (rows.length > 0)
241
262
  return rows;
@@ -121,6 +121,68 @@ function computeRecommendedSizeRaw(targetQuoteRaw, priceRaw30, baseDecimals, quo
121
121
  const denominator = priceRaw30 * (10n ** BigInt(quoteDecimals));
122
122
  return numerator / denominator;
123
123
  }
124
+ function getOrderTypeLabel(orderType) {
125
+ if (orderType === OrderType.MARKET)
126
+ return "MARKET";
127
+ if (orderType === OrderType.LIMIT)
128
+ return "LIMIT";
129
+ if (orderType === OrderType.STOP)
130
+ return "STOP";
131
+ return `ORDER_TYPE_${String(orderType)}`;
132
+ }
133
+ function getDirectionLabel(direction) {
134
+ return direction === 0 ? "LONG" : "SHORT";
135
+ }
136
+ function getTriggerTypeLabel(triggerType) {
137
+ if (triggerType === TriggerType.NONE)
138
+ return "NONE";
139
+ if (triggerType === TriggerType.GTE)
140
+ return "GTE";
141
+ if (triggerType === TriggerType.LTE)
142
+ return "LTE";
143
+ return `TRIGGER_TYPE_${String(triggerType)}`;
144
+ }
145
+ async function validateIncreaseOrderTriggerSemantics(client, args, chainId) {
146
+ const directionIndex = resolveDirectionIndex(args.direction);
147
+ const orderType = Number(args.orderType);
148
+ const explicitTriggerType = args.triggerType;
149
+ if (orderType === OrderType.MARKET) {
150
+ if (explicitTriggerType !== undefined && explicitTriggerType !== null && Number(explicitTriggerType) !== TriggerType.NONE) {
151
+ throw new Error("Invalid triggerType for MARKET open order: MARKET orders must use triggerType=0/NONE.");
152
+ }
153
+ return;
154
+ }
155
+ if (orderType !== OrderType.LIMIT && orderType !== OrderType.STOP) {
156
+ return;
157
+ }
158
+ const expectedTriggerType = resolveTriggerType(orderType, directionIndex, false);
159
+ const effectiveTriggerType = explicitTriggerType === undefined || explicitTriggerType === null
160
+ ? expectedTriggerType
161
+ : Number(explicitTriggerType);
162
+ if (effectiveTriggerType !== expectedTriggerType) {
163
+ throw new Error(`Invalid triggerType for opening ${getDirectionLabel(directionIndex)} ${getOrderTypeLabel(orderType)} order: ` +
164
+ `expected ${getTriggerTypeLabel(expectedTriggerType)}, got ${getTriggerTypeLabel(effectiveTriggerType)}.`);
165
+ }
166
+ const oracleData = await getFreshOraclePrice(client, args.poolId, chainId);
167
+ const currentPriceRaw = ensureUnits(oracleData.price, 30, "oracle price", { allowImplicitRaw: false });
168
+ const targetPriceRaw = BigInt(args.priceRaw30);
169
+ const currentPriceRawBig = BigInt(currentPriceRaw);
170
+ const currentPriceHuman = formatUnits(currentPriceRawBig, 30);
171
+ const targetPriceHuman = formatUnits(targetPriceRaw, 30);
172
+ const orderLabel = `${getDirectionLabel(directionIndex)} ${getOrderTypeLabel(orderType)}`;
173
+ const shouldBeBelowCurrent = (orderType === OrderType.LIMIT && directionIndex === 0) ||
174
+ (orderType === OrderType.STOP && directionIndex === 1);
175
+ const shouldBeAboveCurrent = (orderType === OrderType.STOP && directionIndex === 0) ||
176
+ (orderType === OrderType.LIMIT && directionIndex === 1);
177
+ if (shouldBeBelowCurrent && targetPriceRaw >= currentPriceRawBig) {
178
+ throw new Error(`Invalid ${orderLabel} price: target price ${targetPriceHuman} must be below current oracle price ${currentPriceHuman}. ` +
179
+ `If you want to trade above current price, use ${directionIndex === 0 ? "STOP LONG" : "LIMIT SHORT"} instead.`);
180
+ }
181
+ if (shouldBeAboveCurrent && targetPriceRaw <= currentPriceRawBig) {
182
+ throw new Error(`Invalid ${orderLabel} price: target price ${targetPriceHuman} must be above current oracle price ${currentPriceHuman}. ` +
183
+ `If you want to trade below current price, use ${directionIndex === 0 ? "LIMIT LONG" : "STOP SHORT"} instead.`);
184
+ }
185
+ }
124
186
  function validateIncreaseOrderEconomics(args) {
125
187
  const collateralRawBig = BigInt(args.collateralRaw);
126
188
  const sizeRawBig = BigInt(args.sizeRaw);
@@ -250,6 +312,13 @@ export async function openPosition(client, address, args) {
250
312
  baseDecimals,
251
313
  quoteDecimals,
252
314
  });
315
+ await validateIncreaseOrderTriggerSemantics(client, {
316
+ poolId: args.poolId,
317
+ orderType: Number(args.orderType),
318
+ direction: args.direction,
319
+ triggerType: args.triggerType,
320
+ priceRaw30: priceRaw,
321
+ }, chainId);
253
322
  const timeInForce = mapTimeInForce(args.timeInForce);
254
323
  const orderParams = {
255
324
  chainId,
@@ -13,10 +13,7 @@ export const getTradeFlowTool = {
13
13
  const { client, address } = await resolveClient();
14
14
  const chainId = getChainId();
15
15
  const query = { chainId, poolId: args.poolId, limit: args.limit ?? 20 };
16
- const accessToken = typeof client?.getAccessToken === "function"
17
- ? (await client.getAccessToken()) || ""
18
- : "";
19
- const result = await client.api.getTradeFlow({ ...query, address, accessToken });
16
+ const result = await client.account.getTradeFlow(query, address);
20
17
  const enhancedData = (result?.data || []).map((flow) => ({
21
18
  ...flow,
22
19
  typeDesc: getTradeFlowTypeDesc(flow.type)
@@ -38,6 +38,12 @@ function formatUnixTimestamp(timestamp) {
38
38
  return String(timestamp);
39
39
  return `${timestamp.toString()} (${new Date(numeric * 1000).toISOString()})`;
40
40
  }
41
+ async function withdrawCompat(client, params) {
42
+ if (typeof client?.account?.withdraw === "function") {
43
+ return client.account.withdraw(params);
44
+ }
45
+ return client.account.updateAndWithdraw(params.receiver, params.poolId, params.isQuoteToken, params.amount, params.chainId);
46
+ }
41
47
  export const accountDepositTool = {
42
48
  name: "account_deposit",
43
49
  description: "[ACCOUNT] Deposit funds from wallet into the MYX trading account.",
@@ -136,7 +142,13 @@ export const accountWithdrawTool = {
136
142
  throw new Error(`Account has locked funds until releaseTime=${formatUnixTimestamp(releaseTime)}. ` +
137
143
  `Retry after unlock or reduce withdraw amount.`);
138
144
  }
139
- const raw = await client.account.updateAndWithdraw(address, args.poolId, Boolean(args.isQuoteToken), amount, chainId);
145
+ const raw = await withdrawCompat(client, {
146
+ receiver: address,
147
+ poolId: args.poolId,
148
+ isQuoteToken: Boolean(args.isQuoteToken),
149
+ amount,
150
+ chainId,
151
+ });
140
152
  const data = await finalizeMutationResult(raw, signer, "account_withdraw");
141
153
  const preflight = {
142
154
  requestedAmountRaw: amountRaw.toString(),
@@ -52,7 +52,11 @@ export const cancelOrdersTool = {
52
52
  if (targetOrderIds.length === 0) {
53
53
  return { content: [{ type: "text", text: JSON.stringify({ status: "success", message: "No matching open orders found to cancel." }) }] };
54
54
  }
55
- const raw = await client.order.cancelAllOrders(targetOrderIds, chainId);
55
+ const raw = args.cancelAll
56
+ ? await client.order.cancelAllOrders(targetOrderIds, chainId)
57
+ : targetOrderIds.length === 1
58
+ ? await client.order.cancelOrder(targetOrderIds[0], chainId)
59
+ : await client.order.cancelOrders(targetOrderIds, chainId);
56
60
  const result = await finalizeMutationResult(raw, signer, "cancel_orders");
57
61
  return {
58
62
  content: [{
@@ -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 { resolvePool } from "../services/marketService.js";
4
4
  import { parseUserUnits } from "../utils/units.js";
5
5
  import { ethers } from "ethers";
@@ -49,7 +49,11 @@ export const checkAccountReadyTool = {
49
49
  lockedMarginRaw = toBigIntOrZero(marginInfo.data.lockedMargin);
50
50
  accountWalletBalanceRaw = toBigIntOrZero(marginInfo.data.walletBalance);
51
51
  }
52
- const walletRes = await client.account.getWalletQuoteTokenBalance(chainId, address);
52
+ const walletRes = await client.account.getWalletQuoteTokenBalance({
53
+ chainId,
54
+ address,
55
+ tokenAddress: getQuoteToken(),
56
+ });
53
57
  const walletBalanceRaw = BigInt(walletRes?.data || "0");
54
58
  const requiredRaw = BigInt(parseUserUnits(args.collateralAmount, quoteDecimals, "required"));
55
59
  const isReady = (availableMarginBalanceRaw >= requiredRaw) || (availableMarginBalanceRaw + walletBalanceRaw >= requiredRaw);
@@ -6,6 +6,7 @@ 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
8
  import { parseUserUnits } from "../utils/units.js";
9
+ import { isZeroAddress, normalizeAddress } from "../utils/address.js";
9
10
  const FULL_CLOSE_MARKERS = new Set(["ALL", "FULL", "MAX"]);
10
11
  const INTEGER_RE = /^\d+$/;
11
12
  function wantsFullCloseMarker(input) {
@@ -29,6 +30,20 @@ function resolvePositionRaw(position, rawFields, humanFields, decimals, label) {
29
30
  return "";
30
31
  return parseUserUnits(human, decimals, label);
31
32
  }
33
+ function resolveQuoteExecutionFeeToken(input, quoteToken) {
34
+ const quoteTokenNormalized = normalizeAddress(quoteToken, "quoteToken");
35
+ const raw = String(input ?? "").trim();
36
+ if (!raw)
37
+ return quoteTokenNormalized;
38
+ if (isZeroAddress(raw)) {
39
+ throw new Error(`executionFeeToken cannot be zero address. Use the pool quoteToken address ${quoteTokenNormalized}.`);
40
+ }
41
+ const normalized = normalizeAddress(raw, "executionFeeToken");
42
+ if (normalized.toLowerCase() !== quoteTokenNormalized.toLowerCase()) {
43
+ throw new Error(`executionFeeToken must equal the pool quoteToken ${quoteTokenNormalized}. Native token and non-quote tokens are not supported in MCP trade flows.`);
44
+ }
45
+ return quoteTokenNormalized;
46
+ }
32
47
  export const closePositionTool = {
33
48
  name: "close_position",
34
49
  description: "[TRADE] Create a decrease order (close or reduce position) using SDK-native parameters.",
@@ -39,13 +54,13 @@ export const closePositionTool = {
39
54
  triggerType: z.union([z.number(), z.string()]).optional().describe("Trigger type: 0/NONE, 1/GTE, 2/LTE"),
40
55
  direction: z.union([z.number(), z.string()]).describe("Position direction: 0/LONG or 1/SHORT"),
41
56
  collateralAmount: z.union([z.string(), z.number()]).describe("Collateral amount (human/raw). Also supports ALL/FULL/MAX to use live position collateral raw."),
42
- size: z.union([z.string(), z.number()]).describe("Position size (human/raw). Also supports ALL/FULL/MAX for exact full-close raw size."),
57
+ size: z.union([z.string(), z.number()]).describe("Position size as base asset quantity (human/raw), NOT USD notional. Also supports ALL/FULL/MAX for exact full-close raw size."),
43
58
  price: z.union([z.string(), z.number()]).describe("Price (human or 30-dec raw units)"),
44
- timeInForce: z.union([z.number(), z.string()]).describe("SDK v1.0.4-beta.4 supports IOC only. Use 0 or 'IOC'."),
45
59
  postOnly: z.coerce.boolean().describe("Post-only flag"),
46
60
  slippagePct: z.coerce.string().default("50").describe(SLIPPAGE_PCT_4DP_DESC),
47
- executionFeeToken: z.string().describe("Execution fee token address"),
61
+ executionFeeToken: z.string().optional().describe("Optional. Must equal the pool quoteToken address. Defaults to the pool quoteToken."),
48
62
  leverage: z.coerce.number().describe("Leverage"),
63
+ verify: z.coerce.boolean().optional().describe("If true, wait for backend order-index verification after chain confirmation. Default false for faster responses."),
49
64
  },
50
65
  handler: async (args) => {
51
66
  try {
@@ -93,7 +108,8 @@ export const closePositionTool = {
93
108
  direction: mapDirection(preparedArgs.direction),
94
109
  orderType: mapOrderType(preparedArgs.orderType),
95
110
  triggerType: preparedArgs.triggerType !== undefined ? mapTriggerType(preparedArgs.triggerType) : undefined,
96
- executionFeeToken: poolData.quoteToken || preparedArgs.executionFeeToken
111
+ executionFeeToken: resolveQuoteExecutionFeeToken(preparedArgs.executionFeeToken, String(poolData.quoteToken ?? "")),
112
+ timeInForce: 0,
97
113
  };
98
114
  const positionsRes = await client.position.listPositions(address);
99
115
  const positions = Array.isArray(positionsRes?.data) ? positionsRes.data : [];
@@ -113,10 +129,11 @@ export const closePositionTool = {
113
129
  const data = await finalizeMutationResult(raw, signer, "close_position");
114
130
  const txHash = data.confirmation?.txHash;
115
131
  let verification = null;
116
- if (txHash) {
132
+ const shouldVerify = Boolean(preparedArgs.verify ?? false);
133
+ if (txHash && shouldVerify) {
117
134
  verification = await verifyTradeOutcome(client, address, preparedArgs.poolId, txHash);
118
135
  }
119
- const payload = { ...data, verification };
136
+ const payload = { ...data, verification, verificationSkipped: !!txHash && !shouldVerify };
120
137
  return { content: [{ type: "text", text: JSON.stringify({ status: "success", data: payload }, (_, v) => typeof v === 'bigint' ? v.toString() : v, 2) }] };
121
138
  }
122
139
  catch (error) {
@@ -7,7 +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
+ import { isZeroAddress, normalizeAddress } from "../utils/address.js";
11
11
  const POSITION_ID_RE = /^$|^0x[0-9a-fA-F]{64}$/;
12
12
  const ZERO_POSITION_ID_RE = /^0x0{64}$/i;
13
13
  function pow10(decimals) {
@@ -18,6 +18,20 @@ function computeQuoteNotionalRaw(sizeRaw, priceRaw30, baseDecimals, quoteDecimal
18
18
  const denominator = pow10(baseDecimals + 30);
19
19
  return numerator / denominator;
20
20
  }
21
+ function resolveQuoteExecutionFeeToken(input, quoteToken) {
22
+ const quoteTokenNormalized = normalizeAddress(quoteToken, "quoteToken");
23
+ const raw = String(input ?? "").trim();
24
+ if (!raw)
25
+ return quoteTokenNormalized;
26
+ if (isZeroAddress(raw)) {
27
+ throw new Error(`executionFeeToken cannot be zero address. Use the pool quoteToken address ${quoteTokenNormalized}.`);
28
+ }
29
+ const normalized = normalizeAddress(raw, "executionFeeToken");
30
+ if (normalized.toLowerCase() !== quoteTokenNormalized.toLowerCase()) {
31
+ throw new Error(`executionFeeToken must equal the pool quoteToken ${quoteTokenNormalized}. Native token and non-quote tokens are not supported in MCP trade flows.`);
32
+ }
33
+ return quoteTokenNormalized;
34
+ }
21
35
  export const executeTradeTool = {
22
36
  name: "execute_trade",
23
37
  description: "[TRADE] Create an increase order (open or add to position) using SDK-native parameters.",
@@ -30,12 +44,11 @@ export const executeTradeTool = {
30
44
  triggerType: z.union([z.number(), z.string()]).optional().describe("0=None (Market), 1=GTE, 2=LTE. e.g. 'GTE'."),
31
45
  direction: z.union([z.number(), z.string()]).describe("0/LONG/BUY or 1/SHORT/SELL."),
32
46
  collateralAmount: z.union([z.string(), z.number()]).describe("Collateral. e.g. '100' or 'raw:100000000' (6 decimals for USDC)."),
33
- size: z.union([z.string(), z.number()]).describe("Notional size in base tokens. e.g. '0.5' BTC or 'raw:50000000'."),
47
+ size: z.union([z.string(), z.number()]).describe("Base asset quantity, NOT USD notional. e.g. '0.5' BTC or 'raw:50000000'."),
34
48
  price: z.union([z.string(), z.number()]).describe("Execution or Limit price. e.g. '65000' or 'raw:...'"),
35
- timeInForce: z.union([z.number(), z.string()]).describe("SDK v1.0.4-beta.4 supports IOC only. Use 0 or 'IOC'."),
36
49
  postOnly: z.coerce.boolean().describe("If true, order only executes as Maker."),
37
50
  slippagePct: z.coerce.string().default("50").describe(`${SLIPPAGE_PCT_4DP_DESC}. Default is 50 (0.5%).`),
38
- executionFeeToken: z.string().optional().describe("Address of token to pay gas/execution fees (typically USDC). Default is pool quoteToken."),
51
+ executionFeeToken: z.string().optional().describe("Optional. Must equal the pool quoteToken address. Defaults to the pool quoteToken."),
39
52
  leverage: z.coerce.number().positive().describe("Leverage multiplier, e.g., 10 for 10x."),
40
53
  tpSize: z.union([z.string(), z.number()]).optional().describe("Take Profit size. Use '0' to disable."),
41
54
  tpPrice: z.union([z.string(), z.number()]).optional().describe("Take Profit trigger price."),
@@ -45,6 +58,7 @@ export const executeTradeTool = {
45
58
  assetClass: z.coerce.number().int().nonnegative().optional().describe("Optional fee lookup assetClass (default from pool config or 1)."),
46
59
  riskTier: z.coerce.number().int().nonnegative().optional().describe("Optional fee lookup riskTier (default from pool config or 1)."),
47
60
  marketId: z.string().describe("Specific Market Config Hash. Fetch via get_pool_metadata (resolve poolId first with find_pool/list_pools)."),
61
+ verify: z.coerce.boolean().optional().describe("If true, wait for backend order-index verification after chain confirmation. Default false for faster responses."),
48
62
  },
49
63
  handler: async (args) => {
50
64
  try {
@@ -70,11 +84,7 @@ export const executeTradeTool = {
70
84
  const mappedOrderType = mapOrderType(args.orderType);
71
85
  const mappedTriggerType = args.triggerType !== undefined ? mapTriggerType(args.triggerType) : undefined;
72
86
  const slippagePctNormalized = normalizeSlippagePct4dp(args.slippagePct);
73
- if (args.executionFeeToken !== undefined && args.executionFeeToken !== null && String(args.executionFeeToken).trim() !== "") {
74
- if (isZeroAddress(args.executionFeeToken)) {
75
- throw new Error(`executionFeeToken cannot be zero address. Use the pool quoteToken address ${poolData.quoteToken}.`);
76
- }
77
- }
87
+ const executionFeeToken = resolveQuoteExecutionFeeToken(args.executionFeeToken, String(poolData.quoteToken ?? ""));
78
88
  const collateralRaw = parseUserUnits(args.collateralAmount, quoteDecimals, "collateralAmount");
79
89
  const sizeRaw = parseUserUnits(args.size, baseDecimals, "size");
80
90
  const priceRaw = parseUserUnits(args.price, 30, "price");
@@ -142,13 +152,13 @@ export const executeTradeTool = {
142
152
  triggerType: mappedTriggerType,
143
153
  // Normalize positionId
144
154
  positionId: normalizedPositionId,
145
- // Enforce executionFeeToken as quoteToken
146
- executionFeeToken: poolData.quoteToken || args.executionFeeToken,
155
+ executionFeeToken,
147
156
  collateralAmount: `raw:${collateralRaw}`,
148
157
  size: `raw:${sizeRaw}`,
149
158
  price: `raw:${priceRaw}`,
150
159
  tradingFee: `raw:${tradingFeeRaw}`,
151
160
  slippagePct: slippagePctNormalized,
161
+ timeInForce: 0,
152
162
  };
153
163
  if (args.tpSize !== undefined) {
154
164
  mappedArgs.tpSize = `raw:${parseUserUnits(args.tpSize, baseDecimals, "tpSize")}`;
@@ -166,17 +176,23 @@ export const executeTradeTool = {
166
176
  const data = await finalizeMutationResult(raw, signer, "execute_trade");
167
177
  const txHash = data.confirmation?.txHash;
168
178
  let verification = null;
169
- if (txHash) {
179
+ const shouldVerify = Boolean(args.verify ?? false);
180
+ if (txHash && shouldVerify) {
170
181
  verification = await verifyTradeOutcome(client, address, args.poolId, txHash);
171
182
  }
172
183
  const payload = {
173
184
  ...data,
174
185
  verification,
186
+ verificationSkipped: !!txHash && !shouldVerify,
175
187
  preflight: {
176
188
  normalized: {
177
189
  collateralAmountRaw: collateralRaw,
178
190
  sizeRaw,
179
191
  priceRaw30: priceRaw,
192
+ sizeSemantics: "base_quantity",
193
+ impliedNotionalQuoteRaw: computeQuoteNotionalRaw(BigInt(sizeRaw), BigInt(priceRaw), baseDecimals, quoteDecimals).toString(),
194
+ executionFeeToken,
195
+ timeInForce: mappedArgs.timeInForce,
180
196
  tradingFeeRaw,
181
197
  tpSizeRaw: mappedArgs.tpSize?.replace(/^raw:/i, "") ?? null,
182
198
  tpPriceRaw30: mappedArgs.tpPrice?.replace(/^raw:/i, "") ?? null,
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { resolveClient, getChainId } from "../auth/resolveClient.js";
3
+ import { searchMarketCompat } from "../services/marketService.js";
3
4
  import { normalizeAddress } from "../utils/address.js";
4
5
  import { extractErrorMessage } from "../utils/errorMessage.js";
5
6
  function collectRows(input) {
@@ -14,10 +15,10 @@ function collectRows(input) {
14
15
  function isNonEmptyObject(value) {
15
16
  return !!value && typeof value === "object" && !Array.isArray(value) && Object.keys(value).length > 0;
16
17
  }
17
- async function findBaseDetailFromMarkets(client, chainId, baseAddress) {
18
+ async function loadMarketRows(client, chainId) {
18
19
  let rows = [];
19
20
  try {
20
- const searchRes = await client.markets.searchMarket({ chainId, keyword: "", limit: 2000 });
21
+ const searchRes = await searchMarketCompat(client, { chainId, keyword: "", limit: 2000 });
21
22
  rows = collectRows(searchRes?.data ?? searchRes);
22
23
  }
23
24
  catch {
@@ -32,8 +33,15 @@ async function findBaseDetailFromMarkets(client, chainId, baseAddress) {
32
33
  rows = [];
33
34
  }
34
35
  }
35
- const normalized = baseAddress.toLowerCase();
36
- const matched = rows.find((row) => String(row?.baseToken ?? row?.baseAddress ?? "").trim().toLowerCase() === normalized);
36
+ return rows;
37
+ }
38
+ function findMarketRowByBaseAddress(rows, baseAddress) {
39
+ const normalizedBaseAddress = baseAddress.toLowerCase();
40
+ return rows.find((row) => String(row?.baseToken ?? row?.baseAddress ?? "").trim().toLowerCase() === normalizedBaseAddress);
41
+ }
42
+ async function findBaseDetailFromMarkets(client, chainId, baseAddress) {
43
+ const rows = await loadMarketRows(client, chainId);
44
+ const matched = findMarketRowByBaseAddress(rows, baseAddress);
37
45
  if (!matched)
38
46
  return null;
39
47
  const poolId = String(matched?.poolId ?? matched?.pool_id ?? "").trim();
@@ -83,6 +91,12 @@ async function findBaseDetailFromMarkets(client, chainId, baseAddress) {
83
91
  source: "market_search_fallback",
84
92
  };
85
93
  }
94
+ async function resolvePoolIdByBaseAddress(client, chainId, baseAddress) {
95
+ const rows = await loadMarketRows(client, chainId);
96
+ const matched = findMarketRowByBaseAddress(rows, baseAddress);
97
+ const poolId = String(matched?.poolId ?? matched?.pool_id ?? "").trim();
98
+ return poolId || null;
99
+ }
86
100
  function buildReadErrorPayload(args, messageLike, code = "SDK_READ_ERROR") {
87
101
  const chainId = args.chainId ?? getChainId();
88
102
  const message = extractErrorMessage(messageLike, "Failed to read base token detail.");
@@ -113,7 +127,16 @@ export const getBaseDetailTool = {
113
127
  const { client } = await resolveClient();
114
128
  const chainId = args.chainId ?? getChainId();
115
129
  const baseAddress = normalizeAddress(args.baseAddress, "baseAddress");
116
- const result = await client.markets.getBaseDetail({ chainId, baseAddress });
130
+ const poolId = await resolvePoolIdByBaseAddress(client, chainId, baseAddress);
131
+ if (!poolId) {
132
+ const fallback = await findBaseDetailFromMarkets(client, chainId, baseAddress);
133
+ if (fallback) {
134
+ return { content: [{ type: "text", text: JSON.stringify({ status: "success", data: fallback }, null, 2) }] };
135
+ }
136
+ const body = buildReadErrorPayload({ ...args, baseAddress }, "Could not resolve poolId from baseAddress.", "NOT_FOUND");
137
+ return { content: [{ type: "text", text: JSON.stringify(body, null, 2) }], isError: true };
138
+ }
139
+ const result = await client.markets.getBaseDetail({ chainId, poolId });
117
140
  const hasCode = !!result && typeof result === "object" && !Array.isArray(result) && Object.prototype.hasOwnProperty.call(result, "code");
118
141
  const code = hasCode ? Number(result.code) : 0;
119
142
  const payload = hasCode ? result.data : result;
@@ -3,9 +3,10 @@ import { resolveClient, getChainId } from "../auth/resolveClient.js";
3
3
  import { COMMON_LP_AMOUNT_DECIMALS } from "@myx-trade/sdk";
4
4
  import { Contract, formatUnits } from "ethers";
5
5
  import { extractErrorMessage } from "../utils/errorMessage.js";
6
- import { getPoolList } from "../services/marketService.js";
7
- import { getPoolInfo } from "../services/poolService.js";
6
+ import { getFreshOraclePrice, getPoolList } from "../services/marketService.js";
7
+ import { getLpPrice, getPoolInfo } from "../services/poolService.js";
8
8
  const ADDRESS_RE = /^0x[a-fA-F0-9]{40}$/;
9
+ const INTEGER_RE = /^\d+$/;
9
10
  const ERC20_BALANCE_ABI = ["function balanceOf(address owner) view returns (uint256)"];
10
11
  function collectRows(input) {
11
12
  if (Array.isArray(input))
@@ -127,6 +128,71 @@ async function readErc20Balance(provider, tokenAddress, holder) {
127
128
  const balance = await contract.balanceOf(holder);
128
129
  return BigInt(balance).toString();
129
130
  }
131
+ function pow10(decimals) {
132
+ if (!Number.isInteger(decimals) || decimals < 0) {
133
+ throw new Error(`Invalid decimals: ${decimals}`);
134
+ }
135
+ return 10n ** BigInt(decimals);
136
+ }
137
+ function parseIntegerRaw(value) {
138
+ const text = String(value ?? "").trim();
139
+ if (!INTEGER_RE.test(text))
140
+ return null;
141
+ try {
142
+ return BigInt(text);
143
+ }
144
+ catch {
145
+ return null;
146
+ }
147
+ }
148
+ function formatRawValue(value, decimals) {
149
+ if (!value || !INTEGER_RE.test(value))
150
+ return null;
151
+ try {
152
+ return formatUnits(value, decimals);
153
+ }
154
+ catch {
155
+ return null;
156
+ }
157
+ }
158
+ function computeLpValueQuoteRaw(lpBalanceRaw, lpPriceQuoteRaw30, quoteDecimals) {
159
+ const balance = parseIntegerRaw(lpBalanceRaw);
160
+ const price = parseIntegerRaw(lpPriceQuoteRaw30);
161
+ if (balance === null || price === null)
162
+ return null;
163
+ const valueRaw = (balance * price * pow10(quoteDecimals)) /
164
+ pow10(COMMON_LP_AMOUNT_DECIMALS + 30);
165
+ return valueRaw.toString();
166
+ }
167
+ function computeUnderlyingTokenRawFromExchangeRate(lpBalanceRaw, exchangeRateRaw18, tokenDecimals) {
168
+ const balance = parseIntegerRaw(lpBalanceRaw);
169
+ const exchangeRate = parseIntegerRaw(exchangeRateRaw18);
170
+ if (balance === null || exchangeRate === null)
171
+ return null;
172
+ const underlyingRaw = (balance * exchangeRate * pow10(tokenDecimals)) /
173
+ pow10(COMMON_LP_AMOUNT_DECIMALS + 18);
174
+ return underlyingRaw.toString();
175
+ }
176
+ function computeQuoteValueFromBaseAmountRaw(baseAmountRaw, basePriceRaw30, baseDecimals, quoteDecimals) {
177
+ const baseAmount = parseIntegerRaw(baseAmountRaw);
178
+ const basePrice = parseIntegerRaw(basePriceRaw30);
179
+ if (baseAmount === null || basePrice === null)
180
+ return null;
181
+ const quoteValueRaw = (baseAmount * basePrice * pow10(quoteDecimals)) /
182
+ pow10(baseDecimals + 30);
183
+ return quoteValueRaw.toString();
184
+ }
185
+ function sumRawValues(...values) {
186
+ let total = 0n;
187
+ let found = false;
188
+ for (const value of values) {
189
+ if (!value || !INTEGER_RE.test(value))
190
+ continue;
191
+ total += BigInt(value);
192
+ found = true;
193
+ }
194
+ return found ? total.toString() : null;
195
+ }
130
196
  function collectAddressCandidates(...values) {
131
197
  const unique = new Set();
132
198
  for (const value of values) {
@@ -205,6 +271,9 @@ export const getMyLpHoldingsTool = {
205
271
  const warnings = [];
206
272
  let totalBaseLpRaw = 0n;
207
273
  let totalQuoteLpRaw = 0n;
274
+ let totalBaseEstimatedValueQuoteRaw = 0n;
275
+ let totalQuoteEstimatedValueQuoteRaw = 0n;
276
+ const valuationBuckets = new Map();
208
277
  for (const row of scannedRows) {
209
278
  const poolId = normalizePoolId(row?.poolId ?? row?.pool_id);
210
279
  let basePoolToken = readAddress(row?.basePoolToken ?? row?.base_pool_token);
@@ -259,6 +328,140 @@ export const getMyLpHoldingsTool = {
259
328
  continue;
260
329
  totalBaseLpRaw += BigInt(baseLpRaw);
261
330
  totalQuoteLpRaw += BigInt(quoteLpRaw);
331
+ const baseDecimals = Number(detail?.baseDecimals ?? row?.baseDecimals ?? row?.base_decimals ?? 18);
332
+ const quoteDecimals = Number(detail?.quoteDecimals ?? row?.quoteDecimals ?? row?.quote_decimals ?? 6);
333
+ async function ensurePoolInfoLoaded() {
334
+ if (poolInfo)
335
+ return poolInfo;
336
+ poolInfo = await getPoolInfo(poolId, chainId, client);
337
+ return poolInfo;
338
+ }
339
+ let baseLpPriceQuoteRaw = null;
340
+ let quoteLpPriceQuoteRaw = null;
341
+ let baseEstimatedUnderlyingRaw = null;
342
+ let quoteEstimatedUnderlyingRaw = null;
343
+ let baseEstimatedValueQuoteRaw = null;
344
+ let quoteEstimatedValueQuoteRaw = null;
345
+ let baseValueSource = null;
346
+ let quoteValueSource = null;
347
+ let baseOraclePriceRaw30 = null;
348
+ const baseValuationNotes = [];
349
+ const quoteValuationNotes = [];
350
+ if (BigInt(baseLpRaw) > 0n) {
351
+ try {
352
+ const raw = String(await getLpPrice("BASE", poolId, chainId) ?? "").trim();
353
+ if (INTEGER_RE.test(raw) && BigInt(raw) > 0n) {
354
+ baseLpPriceQuoteRaw = raw;
355
+ baseEstimatedValueQuoteRaw = computeLpValueQuoteRaw(baseLpRaw, raw, quoteDecimals);
356
+ baseValueSource = "sdk.getLpPrice(BASE)";
357
+ }
358
+ }
359
+ catch (error) {
360
+ baseValuationNotes.push(`failed to fetch BASE LP price (${extractErrorMessage(error)})`);
361
+ }
362
+ if (!baseEstimatedValueQuoteRaw) {
363
+ try {
364
+ const info = await ensurePoolInfoLoaded();
365
+ const raw = String(info?.basePool?.poolTokenPrice ?? "").trim();
366
+ if (INTEGER_RE.test(raw) && BigInt(raw) > 0n) {
367
+ baseLpPriceQuoteRaw = raw;
368
+ baseEstimatedValueQuoteRaw = computeLpValueQuoteRaw(baseLpRaw, raw, quoteDecimals);
369
+ baseValueSource = "poolInfo.basePool.poolTokenPrice";
370
+ }
371
+ }
372
+ catch (error) {
373
+ baseValuationNotes.push(`failed to load pool info for BASE valuation (${extractErrorMessage(error)})`);
374
+ }
375
+ }
376
+ if (!baseEstimatedValueQuoteRaw) {
377
+ try {
378
+ const info = await ensurePoolInfoLoaded();
379
+ const exchangeRateRaw = String(info?.basePool?.exchangeRate ?? "").trim();
380
+ baseEstimatedUnderlyingRaw = computeUnderlyingTokenRawFromExchangeRate(baseLpRaw, exchangeRateRaw, baseDecimals);
381
+ if (baseEstimatedUnderlyingRaw && BigInt(baseEstimatedUnderlyingRaw) > 0n) {
382
+ const oracle = await getFreshOraclePrice(client, poolId, chainId);
383
+ baseOraclePriceRaw30 = String(oracle?.price ?? "").trim();
384
+ baseEstimatedValueQuoteRaw = computeQuoteValueFromBaseAmountRaw(baseEstimatedUnderlyingRaw, baseOraclePriceRaw30, baseDecimals, quoteDecimals);
385
+ if (baseEstimatedValueQuoteRaw) {
386
+ baseValueSource = "poolInfo.basePool.exchangeRate * oraclePrice";
387
+ }
388
+ }
389
+ }
390
+ catch (error) {
391
+ baseValuationNotes.push(`failed to estimate BASE LP value from exchangeRate/oracle (${extractErrorMessage(error)})`);
392
+ }
393
+ }
394
+ if (!baseEstimatedValueQuoteRaw) {
395
+ warnings.push(`pool ${poolId}: BASE LP value unavailable (${baseValuationNotes.join("; ") || "no valuation source"})`);
396
+ }
397
+ }
398
+ if (BigInt(quoteLpRaw) > 0n) {
399
+ try {
400
+ const raw = String(await getLpPrice("QUOTE", poolId, chainId) ?? "").trim();
401
+ if (INTEGER_RE.test(raw) && BigInt(raw) > 0n) {
402
+ quoteLpPriceQuoteRaw = raw;
403
+ quoteEstimatedValueQuoteRaw = computeLpValueQuoteRaw(quoteLpRaw, raw, quoteDecimals);
404
+ quoteValueSource = "sdk.getLpPrice(QUOTE)";
405
+ }
406
+ }
407
+ catch (error) {
408
+ quoteValuationNotes.push(`failed to fetch QUOTE LP price (${extractErrorMessage(error)})`);
409
+ }
410
+ if (!quoteEstimatedValueQuoteRaw) {
411
+ try {
412
+ const info = await ensurePoolInfoLoaded();
413
+ const raw = String(info?.quotePool?.poolTokenPrice ?? "").trim();
414
+ if (INTEGER_RE.test(raw) && BigInt(raw) > 0n) {
415
+ quoteLpPriceQuoteRaw = raw;
416
+ quoteEstimatedValueQuoteRaw = computeLpValueQuoteRaw(quoteLpRaw, raw, quoteDecimals);
417
+ quoteValueSource = "poolInfo.quotePool.poolTokenPrice";
418
+ }
419
+ }
420
+ catch (error) {
421
+ quoteValuationNotes.push(`failed to load pool info for QUOTE valuation (${extractErrorMessage(error)})`);
422
+ }
423
+ }
424
+ if (!quoteEstimatedValueQuoteRaw) {
425
+ try {
426
+ const info = await ensurePoolInfoLoaded();
427
+ const exchangeRateRaw = String(info?.quotePool?.exchangeRate ?? "").trim();
428
+ quoteEstimatedUnderlyingRaw = computeUnderlyingTokenRawFromExchangeRate(quoteLpRaw, exchangeRateRaw, quoteDecimals);
429
+ if (quoteEstimatedUnderlyingRaw && BigInt(quoteEstimatedUnderlyingRaw) > 0n) {
430
+ quoteEstimatedValueQuoteRaw = quoteEstimatedUnderlyingRaw;
431
+ quoteValueSource = "poolInfo.quotePool.exchangeRate";
432
+ }
433
+ }
434
+ catch (error) {
435
+ quoteValuationNotes.push(`failed to estimate QUOTE LP value from exchangeRate (${extractErrorMessage(error)})`);
436
+ }
437
+ }
438
+ if (!quoteEstimatedValueQuoteRaw) {
439
+ warnings.push(`pool ${poolId}: QUOTE LP value unavailable (${quoteValuationNotes.join("; ") || "no valuation source"})`);
440
+ }
441
+ }
442
+ const estimatedValueQuoteRaw = sumRawValues(baseEstimatedValueQuoteRaw, quoteEstimatedValueQuoteRaw);
443
+ if (baseEstimatedValueQuoteRaw) {
444
+ totalBaseEstimatedValueQuoteRaw += BigInt(baseEstimatedValueQuoteRaw);
445
+ }
446
+ if (quoteEstimatedValueQuoteRaw) {
447
+ totalQuoteEstimatedValueQuoteRaw += BigInt(quoteEstimatedValueQuoteRaw);
448
+ }
449
+ if (baseEstimatedValueQuoteRaw || quoteEstimatedValueQuoteRaw) {
450
+ const bucketKey = `${quoteSymbol ?? "QUOTE"}:${quoteDecimals}`;
451
+ const bucket = valuationBuckets.get(bucketKey) ?? {
452
+ quoteSymbol,
453
+ quoteDecimals,
454
+ totalBaseEstimatedValueQuoteRaw: 0n,
455
+ totalQuoteEstimatedValueQuoteRaw: 0n,
456
+ };
457
+ if (baseEstimatedValueQuoteRaw) {
458
+ bucket.totalBaseEstimatedValueQuoteRaw += BigInt(baseEstimatedValueQuoteRaw);
459
+ }
460
+ if (quoteEstimatedValueQuoteRaw) {
461
+ bucket.totalQuoteEstimatedValueQuoteRaw += BigInt(quoteEstimatedValueQuoteRaw);
462
+ }
463
+ valuationBuckets.set(bucketKey, bucket);
464
+ }
262
465
  items.push({
263
466
  poolId,
264
467
  symbol: toSymbol(row),
@@ -271,8 +474,26 @@ export const getMyLpHoldingsTool = {
271
474
  quotePoolToken: quotePoolToken ?? null,
272
475
  baseLpBalanceRaw: baseLpRaw,
273
476
  baseLpBalance: formatUnits(baseLpRaw, COMMON_LP_AMOUNT_DECIMALS),
477
+ baseLpPriceQuoteRaw,
478
+ baseLpPriceQuote: formatRawValue(baseLpPriceQuoteRaw, 30),
479
+ baseEstimatedUnderlyingRaw,
480
+ baseEstimatedUnderlying: formatRawValue(baseEstimatedUnderlyingRaw, baseDecimals),
481
+ baseOraclePriceRaw30,
482
+ baseOraclePrice: formatRawValue(baseOraclePriceRaw30, 30),
483
+ baseEstimatedValueQuoteRaw,
484
+ baseEstimatedValueQuote: formatRawValue(baseEstimatedValueQuoteRaw, quoteDecimals),
485
+ baseValueSource,
274
486
  quoteLpBalanceRaw: quoteLpRaw,
275
487
  quoteLpBalance: formatUnits(quoteLpRaw, COMMON_LP_AMOUNT_DECIMALS),
488
+ quoteLpPriceQuoteRaw,
489
+ quoteLpPriceQuote: formatRawValue(quoteLpPriceQuoteRaw, 30),
490
+ quoteEstimatedUnderlyingRaw,
491
+ quoteEstimatedUnderlying: formatRawValue(quoteEstimatedUnderlyingRaw, quoteDecimals),
492
+ quoteEstimatedValueQuoteRaw,
493
+ quoteEstimatedValueQuote: formatRawValue(quoteEstimatedValueQuoteRaw, quoteDecimals),
494
+ quoteValueSource,
495
+ estimatedValueQuoteRaw,
496
+ estimatedValueQuote: formatRawValue(estimatedValueQuoteRaw, quoteDecimals),
276
497
  hasAnyLp,
277
498
  });
278
499
  }
@@ -282,6 +503,20 @@ export const getMyLpHoldingsTool = {
282
503
  return symbolCompare;
283
504
  return String(left.poolId ?? "").localeCompare(String(right.poolId ?? ""));
284
505
  });
506
+ const valuationSummaryByQuote = Array.from(valuationBuckets.values()).map((bucket) => {
507
+ const totalEstimatedValueQuoteRaw = bucket.totalBaseEstimatedValueQuoteRaw + bucket.totalQuoteEstimatedValueQuoteRaw;
508
+ return {
509
+ quoteSymbol: bucket.quoteSymbol,
510
+ quoteDecimals: bucket.quoteDecimals,
511
+ totalBaseEstimatedValueQuoteRaw: bucket.totalBaseEstimatedValueQuoteRaw.toString(),
512
+ totalBaseEstimatedValueQuote: formatUnits(bucket.totalBaseEstimatedValueQuoteRaw, bucket.quoteDecimals),
513
+ totalQuoteEstimatedValueQuoteRaw: bucket.totalQuoteEstimatedValueQuoteRaw.toString(),
514
+ totalQuoteEstimatedValueQuote: formatUnits(bucket.totalQuoteEstimatedValueQuoteRaw, bucket.quoteDecimals),
515
+ totalEstimatedValueQuoteRaw: totalEstimatedValueQuoteRaw.toString(),
516
+ totalEstimatedValueQuote: formatUnits(totalEstimatedValueQuoteRaw, bucket.quoteDecimals),
517
+ };
518
+ });
519
+ const singleQuoteSummary = valuationSummaryByQuote.length === 1 ? valuationSummaryByQuote[0] : null;
285
520
  const payload = {
286
521
  meta: {
287
522
  address,
@@ -298,6 +533,13 @@ export const getMyLpHoldingsTool = {
298
533
  totalBaseLp: formatUnits(totalBaseLpRaw, COMMON_LP_AMOUNT_DECIMALS),
299
534
  totalQuoteLpRaw: totalQuoteLpRaw.toString(),
300
535
  totalQuoteLp: formatUnits(totalQuoteLpRaw, COMMON_LP_AMOUNT_DECIMALS),
536
+ totalBaseEstimatedValueQuoteRaw: totalBaseEstimatedValueQuoteRaw.toString(),
537
+ totalBaseEstimatedValueQuote: singleQuoteSummary?.totalBaseEstimatedValueQuote ?? null,
538
+ totalQuoteEstimatedValueQuoteRaw: totalQuoteEstimatedValueQuoteRaw.toString(),
539
+ totalQuoteEstimatedValueQuote: singleQuoteSummary?.totalQuoteEstimatedValueQuote ?? null,
540
+ totalEstimatedValueQuoteRaw: (totalBaseEstimatedValueQuoteRaw + totalQuoteEstimatedValueQuoteRaw).toString(),
541
+ totalEstimatedValueQuote: singleQuoteSummary?.totalEstimatedValueQuote ?? null,
542
+ valuationSummaryByQuote,
301
543
  },
302
544
  items,
303
545
  };
@@ -227,7 +227,7 @@ function validateTpSlPriceSemantics(direction, entryPriceInput, tpPriceInput, sl
227
227
  }
228
228
  async function cancelTpSlByIntent(client, address, signer, chainId, args) {
229
229
  if (args.orderId) {
230
- const raw = await client.order.cancelAllOrders([String(args.orderId)], chainId);
230
+ const raw = await client.order.cancelOrder(String(args.orderId), chainId);
231
231
  const data = await finalizeMutationResult(raw, signer, "manage_tp_sl_delete");
232
232
  return {
233
233
  content: [{
@@ -264,7 +264,9 @@ async function cancelTpSlByIntent(client, address, signer, chainId, args) {
264
264
  }]
265
265
  };
266
266
  }
267
- const raw = await client.order.cancelAllOrders(orderIds, chainId);
267
+ const raw = orderIds.length === 1
268
+ ? await client.order.cancelOrder(orderIds[0], chainId)
269
+ : await client.order.cancelOrders(orderIds, chainId);
268
270
  const data = await finalizeMutationResult(raw, signer, "manage_tp_sl_delete");
269
271
  return {
270
272
  content: [{
@@ -31,6 +31,14 @@ function computeQuoteNotionalRaw(sizeRaw, priceRaw30, baseDecimals, quoteDecimal
31
31
  const denominator = pow10(baseDecimals + 30);
32
32
  return numerator / denominator;
33
33
  }
34
+ function divRoundUp(numerator, denominator) {
35
+ if (denominator <= 0n) {
36
+ throw new Error("denominator must be > 0.");
37
+ }
38
+ if (numerator <= 0n)
39
+ return 0n;
40
+ return (numerator + denominator - 1n) / denominator;
41
+ }
34
42
  async function getRequiredApprovalSpendRaw(client, marketId, args, chainId) {
35
43
  const networkFeeText = String(await client.utils.getNetworkFee(marketId, chainId) ?? "").trim();
36
44
  if (!/^\d+$/.test(networkFeeText)) {
@@ -58,6 +66,20 @@ function pickMarketDetail(res) {
58
66
  return res;
59
67
  return null;
60
68
  }
69
+ function resolveQuoteExecutionFeeToken(input, quoteToken) {
70
+ const quoteTokenNormalized = normalizeAddress(quoteToken, "quoteToken");
71
+ const raw = String(input ?? "").trim();
72
+ if (!raw)
73
+ return quoteTokenNormalized;
74
+ if (isZeroAddress(raw)) {
75
+ throw new Error(`executionFeeToken cannot be zero address. Use the pool quoteToken address ${quoteTokenNormalized}.`);
76
+ }
77
+ const normalized = normalizeAddress(raw, "executionFeeToken");
78
+ if (normalized.toLowerCase() !== quoteTokenNormalized.toLowerCase()) {
79
+ throw new Error(`executionFeeToken must equal the pool quoteToken ${quoteTokenNormalized}. Native token and non-quote tokens are not supported in MCP trade flows.`);
80
+ }
81
+ return quoteTokenNormalized;
82
+ }
61
83
  export const openPositionSimpleTool = {
62
84
  name: "open_position_simple",
63
85
  description: "[TRADE] High-level open position helper. Computes size/price/tradingFee and submits an increase order. Human units by default; use 'raw:' prefix for raw units.",
@@ -79,13 +101,13 @@ export const openPositionSimpleTool = {
79
101
  size: z.coerce
80
102
  .string()
81
103
  .optional()
82
- .describe("Position size. e.g. '0.5' BTC. If omitted, computed from collateral*leverage/price."),
104
+ .describe("Position size as base asset quantity, NOT USD notional. e.g. '0.5' BTC. If omitted, computed from collateral*leverage/price."),
83
105
  slippagePct: z.coerce
84
106
  .string()
85
107
  .optional()
86
108
  .describe(`${SLIPPAGE_PCT_4DP_DESC} Default 50 (=0.5%).`),
87
109
  postOnly: z.coerce.boolean().optional().describe("Post-only (default false)."),
88
- executionFeeToken: z.string().optional().describe("Execution fee token address (default quoteToken)."),
110
+ executionFeeToken: z.string().optional().describe("Optional. Must equal the pool quoteToken address. Defaults to the pool quoteToken."),
89
111
  assetClass: z.coerce.number().int().nonnegative().optional().describe("Fee query assetClass (default 1)."),
90
112
  riskTier: z.coerce.number().int().nonnegative().optional().describe("Fee query riskTier (default 1)."),
91
113
  tradingFee: z.coerce
@@ -103,6 +125,7 @@ export const openPositionSimpleTool = {
103
125
  .optional()
104
126
  .describe("Deprecated compatibility flag. SDK now handles deposit deltas during order creation."),
105
127
  dryRun: z.coerce.boolean().optional().describe("If true, only compute params; do not send a transaction."),
128
+ verify: z.coerce.boolean().optional().describe("If true, wait for backend order-index verification after chain confirmation. Default false for faster responses."),
106
129
  },
107
130
  handler: async (args) => {
108
131
  try {
@@ -142,12 +165,7 @@ export const openPositionSimpleTool = {
142
165
  const orderType = mapOrderType(args.orderType ?? 0);
143
166
  const postOnly = Boolean(args.postOnly ?? false);
144
167
  const slippagePct = normalizeSlippagePct4dp(args.slippagePct ?? "50");
145
- if (args.executionFeeToken !== undefined && args.executionFeeToken !== null && String(args.executionFeeToken).trim() !== "") {
146
- if (isZeroAddress(args.executionFeeToken)) {
147
- throw new Error(`executionFeeToken cannot be zero address. Use the pool quoteToken address ${quoteToken}.`);
148
- }
149
- }
150
- const executionFeeToken = normalizeAddress(args.executionFeeToken || quoteToken, "executionFeeToken");
168
+ const executionFeeToken = resolveQuoteExecutionFeeToken(args.executionFeeToken, quoteToken);
151
169
  // 4) Determine reference price (30 decimals)
152
170
  let price30;
153
171
  let priceMeta = { source: "user", publishTime: null, oracleType: null, human: null };
@@ -166,6 +184,7 @@ export const openPositionSimpleTool = {
166
184
  const collateralInput = String(args.collateralAmount ?? "").trim();
167
185
  let collateralRaw = "";
168
186
  let collateralRawBig = 0n;
187
+ let collateralMeta = { source: collateralInput ? "user" : "computed" };
169
188
  if (collateralInput) {
170
189
  collateralRaw = parseUserUnits(collateralInput, quoteDecimals, "collateralAmount");
171
190
  collateralRawBig = asBigint(collateralRaw, "collateralAmount");
@@ -195,12 +214,25 @@ export const openPositionSimpleTool = {
195
214
  sizeMeta = { source: "computed", notionalQuoteRaw: notionalQuoteRaw.toString() };
196
215
  }
197
216
  const sizeRawBig = asBigint(sizeRaw, "size");
217
+ const leverageBig = BigInt(args.leverage);
218
+ const impliedNotionalQuoteRaw = computeQuoteNotionalRaw(sizeRawBig, price30Big, baseDecimals, quoteDecimals);
198
219
  if (!collateralInput) {
199
- const notionalQuoteRaw = (sizeRawBig * price30Big * pow10(quoteDecimals)) / pow10(baseDecimals + 30);
200
- const suggestedCollateralRaw = notionalQuoteRaw / BigInt(args.leverage);
201
- const suggestedCollateralHuman = formatUnits(suggestedCollateralRaw, quoteDecimals);
202
- const notionalHuman = formatUnits(notionalQuoteRaw, quoteDecimals);
203
- 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}.`);
220
+ collateralRawBig = divRoundUp(impliedNotionalQuoteRaw, leverageBig);
221
+ if (collateralRawBig <= 0n) {
222
+ throw new Error("Computed collateralAmount is 0. Reduce size, reduce leverage, or check price precision.");
223
+ }
224
+ collateralRaw = collateralRawBig.toString();
225
+ collateralMeta = {
226
+ source: "computed",
227
+ impliedNotionalQuoteRaw: impliedNotionalQuoteRaw.toString(),
228
+ };
229
+ }
230
+ else {
231
+ const maxNotionalFromCollateralRaw = collateralRawBig * leverageBig;
232
+ if (maxNotionalFromCollateralRaw < impliedNotionalQuoteRaw) {
233
+ const requiredCollateralRaw = divRoundUp(impliedNotionalQuoteRaw, leverageBig);
234
+ throw new Error(`collateralAmount is insufficient for open_position_simple. Based on size=${userSize || formatUnits(sizeRawBig, baseDecimals)}, price=${priceMeta.human ?? formatUnits(price30Big, 30)}, leverage=${args.leverage}, implied order value is ≈${formatUnits(impliedNotionalQuoteRaw, quoteDecimals)} quote, provided collateralAmount is ${formatUnits(collateralRawBig, quoteDecimals)}, and required collateralAmount is at least ≈${formatUnits(requiredCollateralRaw, quoteDecimals)}.`);
235
+ }
204
236
  }
205
237
  const maxTradeAmountHuman = String(process.env.MAX_TRADE_AMOUNT ?? "").trim();
206
238
  if (maxTradeAmountHuman) {
@@ -267,6 +299,7 @@ export const openPositionSimpleTool = {
267
299
  tradingFeeMeta,
268
300
  autoDeposit: Boolean(args.autoDeposit ?? false),
269
301
  priceMeta,
302
+ collateralMeta,
270
303
  sizeMeta,
271
304
  tpPrice: args.tpPrice ? parseUserPrice30(args.tpPrice, "tpPrice") : null,
272
305
  tpSize: args.tpSize ? parseUserUnits(args.tpSize, baseDecimals, "tpSize") : (args.tpPrice ? sizeRaw : null),
@@ -338,8 +371,32 @@ export const openPositionSimpleTool = {
338
371
  const raw = await openPosition(client, address, openArgs);
339
372
  const data = await finalizeMutationResult(raw, signer, "open_position_simple");
340
373
  const txHash = data.confirmation?.txHash;
341
- const verification = txHash ? await verifyTradeOutcome(client, address, poolId, txHash) : null;
342
- const payload = { prepared: prep, approval, ...data, verification };
374
+ const shouldVerify = Boolean(args.verify ?? false);
375
+ const verification = txHash && shouldVerify ? await verifyTradeOutcome(client, address, poolId, txHash) : null;
376
+ const payload = {
377
+ prepared: prep,
378
+ approval,
379
+ ...data,
380
+ verification,
381
+ verificationSkipped: !!txHash && !shouldVerify,
382
+ preflight: {
383
+ normalized: {
384
+ collateralAmountRaw: collateralRaw,
385
+ sizeRaw,
386
+ priceRaw30: price30,
387
+ sizeSemantics: "base_quantity",
388
+ impliedNotionalQuoteRaw: impliedNotionalQuoteRaw.toString(),
389
+ executionFeeToken,
390
+ timeInForce: openArgs.timeInForce,
391
+ tradingFeeRaw: String(tradingFeeRaw),
392
+ tpSizeRaw: openArgs.tpSize?.replace(/^raw:/i, "") ?? null,
393
+ tpPriceRaw30: openArgs.tpPrice?.replace(/^raw:/i, "") ?? null,
394
+ slSizeRaw: openArgs.slSize?.replace(/^raw:/i, "") ?? null,
395
+ slPriceRaw30: openArgs.slPrice?.replace(/^raw:/i, "") ?? null,
396
+ },
397
+ tradingFeeMeta,
398
+ },
399
+ };
343
400
  return {
344
401
  content: [
345
402
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@michaleffffff/mcp-trading-server",
3
- "version": "3.1.0",
3
+ "version": "3.1.2",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "myx-mcp": "dist/server.js"
@@ -21,7 +21,7 @@
21
21
  },
22
22
  "dependencies": {
23
23
  "@modelcontextprotocol/sdk": "^1.27.1",
24
- "@myx-trade/sdk": "^1.0.4-beta.4",
24
+ "@myx-trade/sdk": "^1.0.4",
25
25
  "dotenv": "^17.3.1",
26
26
  "ethers": "^6.13.1",
27
27
  "zod": "^4.3.6"