@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 +11 -0
- package/README.md +1 -1
- package/TOOL_EXAMPLES.md +1 -1
- package/dist/auth/resolveClient.js +2 -2
- package/dist/prompts/tradingGuide.js +1 -1
- package/dist/server.js +1 -1
- package/dist/services/marketService.js +32 -11
- package/dist/services/poolService.js +29 -38
- package/dist/tools/getMyLpHoldings.js +2 -1
- package/dist/tools/manageLiquidity.js +1 -1
- package/dist/utils/slippage.js +3 -3
- package/package.json +1 -1
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`
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|
167
|
-
.
|
|
168
|
-
const
|
|
169
|
-
|
|
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 (
|
|
191
|
+
if (orderedMarkets.length > 0) {
|
|
175
192
|
try {
|
|
176
|
-
const poolIds =
|
|
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
|
|
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
|
|
283
|
-
|
|
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: "
|
|
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: "
|
|
29
|
-
basePool: "
|
|
30
|
-
quotePool: "
|
|
28
|
+
router: "0x0fb875c10fe2fF981e467765E3daaE4355b180D0",
|
|
29
|
+
basePool: "0xeC0b3C76cC1C47f9B29313c706c1F6FD8D1C023f",
|
|
30
|
+
quotePool: "0x608Bfd6B8Df0807aA6d1500B76a3eAb3C9DFd728",
|
|
31
31
|
},
|
|
32
32
|
59141: {
|
|
33
|
-
router: "
|
|
34
|
-
basePool: "
|
|
35
|
-
quotePool: "
|
|
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 >
|
|
195
|
-
return slippage /
|
|
196
|
-
if (slippage
|
|
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
|
-
|
|
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 = [
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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
|
-
|
|
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 = [
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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 =
|
|
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().
|
|
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) => {
|
package/dist/utils/slippage.js
CHANGED
|
@@ -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
|
|
50
|
-
throw new Error(`${label} must be a finite
|
|
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
|
|
52
|
+
return numeric;
|
|
53
53
|
}
|