@michaleffffff/mcp-trading-server 3.0.29 → 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,16 @@
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
+
3
14
  ## [3.0.29] - 2026-03-20
4
15
  - **Fix**: Implemented oracle validation bypass for State 0 (Cook) and State 1 (Primed) pools in liquidity management.
5
16
  - This allows adding/removing liquidity on newly created pools that haven't initialized their oracle feeds yet.
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.29" }, { 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)
@@ -327,6 +327,7 @@ async function executeLiquidityTxViaRouter(params) {
327
327
  const amountOut = await previewAmountOutForLiquidity(signer, chainId, poolId, poolType, action, amountIn, oraclePayload.referencePrice30);
328
328
  const minAmountOut = applyMinOutBySlippage(amountOut, slippage);
329
329
  const routerContract = new Contract(addresses.router, ROUTER_ABI, signer);
330
+ const oraclePriceTuples = oraclePayload.prices;
330
331
  const txOverrides = {};
331
332
  if (oraclePayload.value > 0n) {
332
333
  txOverrides.value = oraclePayload.value;
@@ -340,21 +341,16 @@ async function executeLiquidityTxViaRouter(params) {
340
341
  address,
341
342
  [],
342
343
  ];
343
- if (oraclePayload.prices.length > 0) {
344
- tx = await routerContract["depositQuote((bytes32,uint8,uint64,bytes)[],(bytes32,uint256,uint256,address,(uint256,uint256,uint8,uint256)[]))"](oraclePayload.prices, paramsTuple, txOverrides);
345
- }
346
- else {
347
- tx = await routerContract["depositQuote((bytes32,uint256,uint256,address,(uint256,uint256,uint8,uint256)[]))"](paramsTuple, txOverrides);
348
- }
344
+ tx = await routerContract["depositQuote((bytes32,uint8,uint64,bytes)[],(bytes32,uint256,uint256,address,(uint256,uint256,uint8,uint256)[]))"](oraclePriceTuples, paramsTuple, txOverrides);
349
345
  }
350
346
  else if (poolType === "QUOTE" && action === "withdraw") {
351
- const paramsTuple = [poolId, amountIn, minAmountOut, address];
352
- if (oraclePayload.prices.length > 0) {
353
- tx = await routerContract["withdrawQuote((bytes32,uint8,uint64,bytes)[],(bytes32,uint256,uint256,address))"](oraclePayload.prices, paramsTuple, txOverrides);
354
- }
355
- else {
356
- tx = await routerContract["withdrawQuote((bytes32,uint256,uint256,address))"](paramsTuple, txOverrides);
357
- }
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);
358
354
  }
359
355
  else if (poolType === "BASE" && action === "deposit") {
360
356
  const paramsTuple = [
@@ -364,21 +360,16 @@ async function executeLiquidityTxViaRouter(params) {
364
360
  address,
365
361
  [],
366
362
  ];
367
- if (oraclePayload.prices.length > 0) {
368
- tx = await routerContract["depositBase((bytes32,uint8,uint64,bytes)[],(bytes32,uint256,uint256,address,(uint256,uint256,uint8,uint256)[]))"](oraclePayload.prices, paramsTuple, txOverrides);
369
- }
370
- else {
371
- tx = await routerContract["depositBase((bytes32,uint256,uint256,address,(uint256,uint256,uint8,uint256)[]))"](paramsTuple, txOverrides);
372
- }
363
+ tx = await routerContract["depositBase((bytes32,uint8,uint64,bytes)[],(bytes32,uint256,uint256,address,(uint256,uint256,uint8,uint256)[]))"](oraclePriceTuples, paramsTuple, txOverrides);
373
364
  }
374
365
  else {
375
- const paramsTuple = [poolId, amountIn, minAmountOut, address];
376
- if (oraclePayload.prices.length > 0) {
377
- tx = await routerContract["withdrawBase((bytes32,uint8,uint64,bytes)[],(bytes32,uint256,uint256,address))"](oraclePayload.prices, paramsTuple, txOverrides);
378
- }
379
- else {
380
- tx = await routerContract["withdrawBase((bytes32,uint256,uint256,address))"](paramsTuple, txOverrides);
381
- }
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);
382
373
  }
383
374
  const txHash = String(tx?.hash ?? "").trim();
384
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.29",
3
+ "version": "3.0.31",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "myx-mcp": "dist/server.js"