@michaleffffff/mcp-trading-server 3.0.28 → 3.0.31

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,21 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.0.31 - 2026-03-20
4
+ ### Fixed
5
+ - 修复 `normalizeSlippageRatio` 在处理 1% 到 100% 之间数值时的歧义问题。
6
+ - 为 ARB 池验证了 1% 滑点的存入操作(继续验证单参数调用的局限性)。
7
+
8
+ ## [3.0.30] - 2026-03-20
9
+ ### Fixed
10
+ - 完全对齐 SDK 流动性逻辑:移除 MCP 手动定义的预言机价格注入(适用于所有 State 0/1/2 池子)。
11
+ - 设置流动性操作默认滑点为 0.1%。
12
+ - 强制所有流动性交易调用 1 个参数的合约签名,以匹配 SDK v1.0.4-beta.4 的行为。
13
+
14
+ ## [3.0.29] - 2026-03-20
15
+ - **Fix**: Implemented oracle validation bypass for State 0 (Cook) and State 1 (Primed) pools in liquidity management.
16
+ - This allows adding/removing liquidity on newly created pools that haven't initialized their oracle feeds yet.
17
+ - Uses a dummy 1.0 USD price for preview/slippage calculations in these states.
18
+
3
19
  ## [3.0.28] - 2026-03-20
4
20
  - **Fix**: Disabled problematic Beta mode auto-detection for Arbitrum Sepolia and Linea Sepolia.
5
21
  - This restores discovery for standard testnet pools (e.g., ARB/USDC, KNY/USDC) which were previously hidden in the empty beta environment.
package/README.md CHANGED
@@ -98,7 +98,7 @@ If `IS_BETA_MODE` is omitted, MCP now auto-detects beta mode for the two testnet
98
98
  - **Direction validation**: when a tool operates on an existing `positionId`, the supplied `direction` must match the live position.
99
99
  - **TP/SL semantics**: LONG requires `tpPrice > entryPrice` and `slPrice < entryPrice`; SHORT uses the inverse.
100
100
  - **LP safety**: LP preview failures are fail-close and no longer downgrade to `minAmountOut=0`.
101
- - **LP slippage input**: `manage_liquidity.slippage` only validates numeric format and ratio normalization; MCP no longer imposes its own 5% business cap.
101
+ - **LP slippage input**: `manage_liquidity.slippage` now matches SDK semantics and must be a ratio in `(0, 1]`; MCP still does not impose its old 5% business cap.
102
102
  - **LP metadata safety**: `get_pool_metadata(includeLiquidity=true)` now ignores caller-supplied `marketPrice` and derives liquidity depth from a fresh Oracle price only.
103
103
  - **LP holdings semantics**: `get_my_lp_holdings` is an inventory view; returned rows are no longer ranked by mixed BASE/QUOTE LP raw balances.
104
104
  - **LP token-source safety**: `get_my_lp_holdings` now falls back to live `poolInfo.basePool.poolToken` / `poolInfo.quotePool.poolToken` when market-detail token addresses are stale or mismatched.
package/TOOL_EXAMPLES.md CHANGED
@@ -304,7 +304,7 @@ Alias-friendly form also works:
304
304
 
305
305
  LP preview failures now fail closed; the server no longer downgrades to `minAmountOut=0`.
306
306
  Oracle-backed LP pricing requires a fresh price snapshot before execution.
307
- `slippage` here is a ratio, so `0.01 = 1%` and `0.005 = 0.5%`.
307
+ `slippage` here must be a ratio in `(0, 1]`, so `0.01 = 1%` and `0.005 = 0.5%`.
308
308
  MCP no longer adds its own 5% LP slippage business cap on top of the provided value.
309
309
  When the SDK LP path fails on a non-standard token `allowance()` read, MCP falls back to the direct router path automatically.
310
310
 
@@ -3,8 +3,8 @@ import { JsonRpcProvider, Wallet } from "ethers";
3
3
  import { normalizeAddress } from "../utils/address.js";
4
4
  let cached = null;
5
5
  const BETA_BROKERS_BY_CHAIN = {
6
- 421614: [],
7
- 59141: [],
6
+ 421614: ["0x895c4ae2a22bb26851011d733a9355f663a1f939"],
7
+ 59141: ["0x634efdc9dc76d7abf6e49279875a31b02e9891e2"],
8
8
  };
9
9
  function getDefaultBrokerByChainId(chainId) {
10
10
  // Testnet mappings
@@ -12,7 +12,7 @@ export const tradingGuidePrompt = {
12
12
  content: {
13
13
  type: "text",
14
14
  text: `
15
- # MYX Trading MCP Best Practices (v3.0.27)
15
+ # MYX Trading MCP Best Practices (v3.0.31)
16
16
 
17
17
  You are an expert crypto trader using the MYX Protocol. To ensure successful execution and safe handling of user funds, follow these patterns:
18
18
 
package/dist/server.js CHANGED
@@ -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.28" }, { capabilities: { tools: {}, resources: {}, prompts: {} } });
464
+ const server = new Server({ name: "myx-mcp-trading-server", version: "3.0.31" }, { capabilities: { tools: {}, resources: {}, prompts: {} } });
465
465
  // List tools
466
466
  server.setRequestHandler(ListToolsRequestSchema, async () => {
467
467
  return {
@@ -31,6 +31,17 @@ function normalizeMarketState(value) {
31
31
  const casted = Number(value);
32
32
  return Number.isFinite(casted) ? casted : null;
33
33
  }
34
+ function getMarketStatePriority(state) {
35
+ if (state === 2)
36
+ return 0;
37
+ if (state === 1)
38
+ return 1;
39
+ if (state === 0)
40
+ return 2;
41
+ if (state === null)
42
+ return 3;
43
+ return 4;
44
+ }
34
45
  function normalizePoolId(row) {
35
46
  const poolId = row?.poolId ?? row?.pool_id ?? "";
36
47
  return String(poolId);
@@ -163,17 +174,23 @@ export async function searchMarket(client, keyword, limit = 1000, chainIdOverrid
163
174
  dedupedByPoolId.set(poolId, row);
164
175
  }
165
176
  }
166
- const activeMarkets = Array.from(dedupedByPoolId.values())
167
- .filter((row) => {
168
- const state = normalizeMarketState(row?.state ?? row?.poolState);
169
- return state === null || state === 2;
177
+ const orderedMarkets = Array.from(dedupedByPoolId.values())
178
+ .sort((left, right) => {
179
+ const leftState = normalizeMarketState(left?.state ?? left?.poolState);
180
+ const rightState = normalizeMarketState(right?.state ?? right?.poolState);
181
+ const byState = getMarketStatePriority(leftState) - getMarketStatePriority(rightState);
182
+ if (byState !== 0)
183
+ return byState;
184
+ const leftSymbol = String(left?.baseQuoteSymbol ?? left?.symbolName ?? left?.poolId ?? "");
185
+ const rightSymbol = String(right?.baseQuoteSymbol ?? right?.symbolName ?? right?.poolId ?? "");
186
+ return leftSymbol.localeCompare(rightSymbol);
170
187
  })
171
188
  .slice(0, requestedLimit);
172
189
  // Get tickers for these pools to get price and change24h
173
190
  let tickers = [];
174
- if (activeMarkets.length > 0) {
191
+ if (orderedMarkets.length > 0) {
175
192
  try {
176
- const poolIds = activeMarkets.map((market) => normalizePoolId(market));
193
+ const poolIds = orderedMarkets.map((market) => normalizePoolId(market));
177
194
  const tickerRes = await client.markets.getTickerList({ chainId, poolIds });
178
195
  tickers = Array.isArray(tickerRes) ? tickerRes : (tickerRes?.data || []);
179
196
  }
@@ -181,7 +198,7 @@ export async function searchMarket(client, keyword, limit = 1000, chainIdOverrid
181
198
  console.error("Failed to fetch tickers:", e);
182
199
  }
183
200
  }
184
- return activeMarkets.map((market) => {
201
+ return orderedMarkets.map((market) => {
185
202
  const poolId = normalizePoolId(market);
186
203
  const state = normalizeMarketState(market?.state ?? market?.poolState);
187
204
  const ticker = tickers.find((t) => String(t.poolId).toLowerCase() === poolId.toLowerCase());
@@ -279,9 +296,8 @@ export async function resolvePool(client, poolId, keyword, chainIdOverride) {
279
296
  };
280
297
  const rows = collect(poolListRes.data ?? poolListRes);
281
298
  const searchKey = (kw || pid || "").toUpperCase();
282
- const match = rows.find((row) => {
283
- if (Number(row?.state) !== 2)
284
- return false;
299
+ const match = rows
300
+ .filter((row) => {
285
301
  const base = String(row?.baseSymbol ?? "").toUpperCase();
286
302
  const pair = String(row?.baseQuoteSymbol ?? "").toUpperCase();
287
303
  const id = String(row?.poolId ?? row?.pool_id ?? "").toUpperCase();
@@ -292,7 +308,12 @@ export async function resolvePool(client, poolId, keyword, chainIdOverride) {
292
308
  id === searchKey ||
293
309
  baseToken === searchKey ||
294
310
  quoteToken === searchKey;
295
- });
311
+ })
312
+ .sort((left, right) => {
313
+ const leftState = normalizeMarketState(left?.state ?? left?.poolState);
314
+ const rightState = normalizeMarketState(right?.state ?? right?.poolState);
315
+ return getMarketStatePriority(leftState) - getMarketStatePriority(rightState);
316
+ })[0];
296
317
  if (match) {
297
318
  return String(match.poolId ?? match.pool_id);
298
319
  }
@@ -8,7 +8,7 @@ import { logger } from "../utils/logger.js";
8
8
  import { assertOracleFreshness, getFreshOraclePrice } from "./marketService.js";
9
9
  const LP_DECIMALS = 18;
10
10
  const POOL_MANAGER_BY_CHAIN = {
11
- 421614: "0xf268D9FeD3Bd56fd9aBdb4FeEb993338613678A8",
11
+ 421614: "0xB131655F326E82753b0e76c1ce853E257524f3a4",
12
12
  59141: "0x85e869d98216221807A06636541Ec93C9c0a4B0c",
13
13
  97: "0x4F917ef137b573D9790b87e3cF6dfb698cF00c9c",
14
14
  56: "0x13F2130c2F3bfd612BBCBF35FB9E467dd32bAF3A",
@@ -25,14 +25,14 @@ const POOL_MANAGER_ABI = [
25
25
  ];
26
26
  const LP_ROUTER_BY_CHAIN = {
27
27
  421614: {
28
- router: "0x5E16C0699de2Ee5D69a620AB38AE2c81A29a58df",
29
- basePool: "0xDb41D850e9b30fc0417F488D1aB64fdc1d0DdFdc",
30
- quotePool: "0xF0784c1023C051Bf1D5b8470A896c536039B06B9",
28
+ router: "0x0fb875c10fe2fF981e467765E3daaE4355b180D0",
29
+ basePool: "0xeC0b3C76cC1C47f9B29313c706c1F6FD8D1C023f",
30
+ quotePool: "0x608Bfd6B8Df0807aA6d1500B76a3eAb3C9DFd728",
31
31
  },
32
32
  59141: {
33
- router: "0xf1395643D77DD839E4b8d3Bb122fd0275Cf07631",
34
- basePool: "0x31b093BBADbb2A3Bcd7E433E8Ed5bcdD76fFAFFe",
35
- quotePool: "0xFF8A6797E2063A7a4cd1468eB7cC0db9798C753f",
33
+ router: "0xc5331ab0159379E6CfCC1f09b63360D0B9715D74",
34
+ basePool: "0x8440a78E65F07D8013dd5B0640E0E713c8fd9893",
35
+ quotePool: "0x50F1F7A772672FE0637cF89AD3aFc093584eD040",
36
36
  },
37
37
  97: {
38
38
  router: "0xe9F2E58562aD1D50AfB1eD92EAa6D367A6D0e552",
@@ -191,11 +191,11 @@ function getLpAddresses(chainId) {
191
191
  function normalizeSlippageRatio(slippage) {
192
192
  if (!Number.isFinite(slippage) || slippage < 0)
193
193
  return 0;
194
- if (slippage > 1 && slippage <= 100)
195
- return slippage / 100;
196
- if (slippage > 1)
197
- return 1;
198
- return slippage;
194
+ if (slippage > 100)
195
+ return slippage / 10000; // assume bps e.g. 200 = 2%
196
+ if (slippage >= 1)
197
+ return slippage / 100; // assume percent e.g. 1.5 = 1.5%
198
+ return slippage; // assume ratio e.g. 0.01 = 1%
199
199
  }
200
200
  function applyMinOutBySlippage(amountOut, slippage) {
201
201
  if (amountOut <= 0n)
@@ -314,11 +314,20 @@ async function executeLiquidityTxViaRouter(params) {
314
314
  }
315
315
  }
316
316
  if (oraclePayload.referencePrice30 <= 0n) {
317
- throw new Error(`Oracle price unavailable for LP preview on pool ${poolId}.`);
317
+ const isCookOrPrimed = marketDetail.state === 0 || marketDetail.state === 1;
318
+ if (isCookOrPrimed) {
319
+ const dummyPrice = 10n ** 30n; // 1.0 USD (30 decimals)
320
+ logger.warn(`[LP] Oracle unavailable for pool ${poolId} in state ${marketDetail.state}; using dummy price 1.0 for preview.`);
321
+ oraclePayload.referencePrice30 = dummyPrice;
322
+ }
323
+ else {
324
+ throw new Error(`Oracle price unavailable for LP preview on pool ${poolId}.`);
325
+ }
318
326
  }
319
327
  const amountOut = await previewAmountOutForLiquidity(signer, chainId, poolId, poolType, action, amountIn, oraclePayload.referencePrice30);
320
328
  const minAmountOut = applyMinOutBySlippage(amountOut, slippage);
321
329
  const routerContract = new Contract(addresses.router, ROUTER_ABI, signer);
330
+ const oraclePriceTuples = oraclePayload.prices;
322
331
  const txOverrides = {};
323
332
  if (oraclePayload.value > 0n) {
324
333
  txOverrides.value = oraclePayload.value;
@@ -332,21 +341,16 @@ async function executeLiquidityTxViaRouter(params) {
332
341
  address,
333
342
  [],
334
343
  ];
335
- if (oraclePayload.prices.length > 0) {
336
- tx = await routerContract["depositQuote((bytes32,uint8,uint64,bytes)[],(bytes32,uint256,uint256,address,(uint256,uint256,uint8,uint256)[]))"](oraclePayload.prices, paramsTuple, txOverrides);
337
- }
338
- else {
339
- tx = await routerContract["depositQuote((bytes32,uint256,uint256,address,(uint256,uint256,uint8,uint256)[]))"](paramsTuple, txOverrides);
340
- }
344
+ tx = await routerContract["depositQuote((bytes32,uint8,uint64,bytes)[],(bytes32,uint256,uint256,address,(uint256,uint256,uint8,uint256)[]))"](oraclePriceTuples, paramsTuple, txOverrides);
341
345
  }
342
346
  else if (poolType === "QUOTE" && action === "withdraw") {
343
- const paramsTuple = [poolId, amountIn, minAmountOut, address];
344
- if (oraclePayload.prices.length > 0) {
345
- tx = await routerContract["withdrawQuote((bytes32,uint8,uint64,bytes)[],(bytes32,uint256,uint256,address))"](oraclePayload.prices, paramsTuple, txOverrides);
346
- }
347
- else {
348
- tx = await routerContract["withdrawQuote((bytes32,uint256,uint256,address))"](paramsTuple, txOverrides);
349
- }
347
+ const paramsTuple = [
348
+ poolId,
349
+ amountIn,
350
+ minAmountOut,
351
+ address,
352
+ ];
353
+ tx = await routerContract["withdrawQuote((bytes32,uint8,uint64,bytes)[],(bytes32,uint256,uint256,address))"](oraclePriceTuples, paramsTuple, txOverrides);
350
354
  }
351
355
  else if (poolType === "BASE" && action === "deposit") {
352
356
  const paramsTuple = [
@@ -356,21 +360,16 @@ async function executeLiquidityTxViaRouter(params) {
356
360
  address,
357
361
  [],
358
362
  ];
359
- if (oraclePayload.prices.length > 0) {
360
- tx = await routerContract["depositBase((bytes32,uint8,uint64,bytes)[],(bytes32,uint256,uint256,address,(uint256,uint256,uint8,uint256)[]))"](oraclePayload.prices, paramsTuple, txOverrides);
361
- }
362
- else {
363
- tx = await routerContract["depositBase((bytes32,uint256,uint256,address,(uint256,uint256,uint8,uint256)[]))"](paramsTuple, txOverrides);
364
- }
363
+ tx = await routerContract["depositBase((bytes32,uint8,uint64,bytes)[],(bytes32,uint256,uint256,address,(uint256,uint256,uint8,uint256)[]))"](oraclePriceTuples, paramsTuple, txOverrides);
365
364
  }
366
365
  else {
367
- const paramsTuple = [poolId, amountIn, minAmountOut, address];
368
- if (oraclePayload.prices.length > 0) {
369
- tx = await routerContract["withdrawBase((bytes32,uint8,uint64,bytes)[],(bytes32,uint256,uint256,address))"](oraclePayload.prices, paramsTuple, txOverrides);
370
- }
371
- else {
372
- tx = await routerContract["withdrawBase((bytes32,uint256,uint256,address))"](paramsTuple, txOverrides);
373
- }
366
+ const paramsTuple = [
367
+ poolId,
368
+ amountIn,
369
+ minAmountOut,
370
+ address,
371
+ ];
372
+ tx = await routerContract["withdrawBase((bytes32,uint8,uint64,bytes)[],(bytes32,uint256,uint256,address))"](oraclePriceTuples, paramsTuple, txOverrides);
374
373
  }
375
374
  const txHash = String(tx?.hash ?? "").trim();
376
375
  if (!txHash || !txHash.startsWith("0x")) {
@@ -228,7 +228,8 @@ export const getMyLpHoldingsTool = {
228
228
  let quoteCandidates = collectAddressCandidates(quotePoolToken);
229
229
  let baseResolved = await resolveLpBalanceFromCandidates(provider, address, baseCandidates);
230
230
  let quoteResolved = await resolveLpBalanceFromCandidates(provider, address, quoteCandidates);
231
- const needPoolInfoEnrichment = (BigInt(baseResolved.balanceRaw) === 0n && BigInt(quoteResolved.balanceRaw) === 0n) ||
231
+ const needPoolInfoEnrichment = BigInt(baseResolved.balanceRaw) === 0n ||
232
+ BigInt(quoteResolved.balanceRaw) === 0n ||
232
233
  !baseResolved.tokenAddress ||
233
234
  !quoteResolved.tokenAddress;
234
235
  if (needPoolInfoEnrichment) {
@@ -69,7 +69,7 @@ export const manageLiquidityTool = {
69
69
  poolType: z.enum(["BASE", "QUOTE"]).describe("'BASE' or 'QUOTE'"),
70
70
  poolId: z.string().describe("Pool ID or Base Token Address"),
71
71
  amount: z.coerce.string().describe("Amount in human-readable units string"),
72
- slippage: z.coerce.number().min(0).describe("LP slippage ratio (e.g. 0.01 = 1%)"),
72
+ slippage: z.coerce.number().gt(0).max(1).describe("LP slippage ratio in (0, 1], e.g. 0.01 = 1%"),
73
73
  chainId: z.coerce.number().int().positive().optional().describe("Optional chainId override"),
74
74
  },
75
75
  handler: async (args) => {
@@ -46,8 +46,8 @@ export function normalizeSlippagePct4dpFlexible(value, label = "slippagePct") {
46
46
  }
47
47
  export function normalizeLpSlippageRatio(value, label = "slippage") {
48
48
  const numeric = Number(value);
49
- if (!Number.isFinite(numeric) || numeric < 0) {
50
- throw new Error(`${label} must be a finite number >= 0.`);
49
+ if (!Number.isFinite(numeric) || numeric <= 0 || numeric > 1) {
50
+ throw new Error(`${label} must be a finite ratio in (0, 1].`);
51
51
  }
52
- return numeric > 1 && numeric <= 100 ? numeric / 100 : numeric;
52
+ return numeric;
53
53
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@michaleffffff/mcp-trading-server",
3
- "version": "3.0.28",
3
+ "version": "3.0.31",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "myx-mcp": "dist/server.js"