@michaleffffff/mcp-trading-server 3.0.26 → 3.0.28
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 +19 -1
- package/README.md +3 -0
- package/TOOL_EXAMPLES.md +4 -0
- package/dist/auth/resolveClient.js +2 -2
- package/dist/prompts/tradingGuide.js +3 -1
- package/dist/server.js +1 -1
- package/dist/services/poolService.js +36 -5
- package/dist/tools/getMyLpHoldings.js +59 -12
- package/dist/utils/slippage.js +1 -6
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [3.0.28] - 2026-03-20
|
|
4
|
+
- **Fix**: Disabled problematic Beta mode auto-detection for Arbitrum Sepolia and Linea Sepolia.
|
|
5
|
+
- This restores discovery for standard testnet pools (e.g., ARB/USDC, KNY/USDC) which were previously hidden in the empty beta environment.
|
|
6
|
+
- Users can still manually override via `IS_BETA_MODE=true` in `.env`.
|
|
7
|
+
|
|
8
|
+
## 3.0.27 - 2026-03-20
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- Hardened LP MCP behavior for mismatched token metadata and non-standard ERC20 approvals:
|
|
12
|
+
- `get_my_lp_holdings` now retries balance discovery against live `poolInfo` LP token addresses when market detail addresses are stale.
|
|
13
|
+
- LP router fallback now treats SDK `allowance()` read reverts as recoverable and attempts direct router approval/execution.
|
|
14
|
+
- Removed the MCP-side 5% LP slippage business cap; `manage_liquidity.slippage` now only normalizes ratio input and leaves risk tolerance to the caller / downstream contracts.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
- Refreshed operator docs and examples to match current LP execution semantics:
|
|
18
|
+
- `TOOL_EXAMPLES.md` clarifies LP `slippage` ratio semantics and removal of the business cap.
|
|
19
|
+
- `trading_best_practices` prompt now aligns with release `v3.0.27`.
|
|
20
|
+
|
|
3
21
|
## 3.0.26 - 2026-03-20
|
|
4
22
|
|
|
5
23
|
### Fixed
|
|
@@ -14,7 +32,7 @@
|
|
|
14
32
|
- Refreshed operator docs and examples to match current execution semantics:
|
|
15
33
|
- `README.md` now documents explicit `price` in the quick-start trade example, SDK-delegated increase-order funding deltas, and the new `check_account_ready` degraded diagnostics.
|
|
16
34
|
- `TOOL_EXAMPLES.md` now marks `autoDeposit` as deprecated and updates `MARKET` examples to provide `price`.
|
|
17
|
-
- `trading_best_practices` prompt now explains that pre-checks use SDK `availableMarginBalance
|
|
35
|
+
- `trading_best_practices` prompt now explains that pre-checks use SDK `availableMarginBalance`, that increase-order funding reconciliation lives in the SDK, and that LP holdings may be resolved from live pool token addresses.
|
|
18
36
|
- Updated stale tests to match current MCP semantics:
|
|
19
37
|
- `tests/test_trading.ts` now provides `price` for `open_position_simple` market dry runs.
|
|
20
38
|
- `tests/verify_tp_sl_close_invalid_param.mjs` no longer relies on legacy `autoDeposit` semantics and now supplies `price` when opening a market position.
|
package/README.md
CHANGED
|
@@ -98,8 +98,11 @@ 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
102
|
- **LP metadata safety**: `get_pool_metadata(includeLiquidity=true)` now ignores caller-supplied `marketPrice` and derives liquidity depth from a fresh Oracle price only.
|
|
102
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
|
+
- **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.
|
|
105
|
+
- **LP approval fallback**: when SDK LP deposit paths fail on non-standard `allowance()` reads, MCP falls back to direct router approval/execution instead of stopping at the SDK error.
|
|
103
106
|
|
|
104
107
|
---
|
|
105
108
|
|
package/TOOL_EXAMPLES.md
CHANGED
|
@@ -304,6 +304,9 @@ 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%`.
|
|
308
|
+
MCP no longer adds its own 5% LP slippage business cap on top of the provided value.
|
|
309
|
+
When the SDK LP path fails on a non-standard token `allowance()` read, MCP falls back to the direct router path automatically.
|
|
307
310
|
|
|
308
311
|
### `get_lp_price`
|
|
309
312
|
Read LP NAV price for BASE or QUOTE side.
|
|
@@ -334,6 +337,7 @@ Read current LP balances across pools.
|
|
|
334
337
|
```
|
|
335
338
|
|
|
336
339
|
`get_my_lp_holdings` is an inventory view. Results are now stably ordered by symbol / pool id instead of summing BASE LP raw units with QUOTE LP raw units into a misleading mixed-unit ranking.
|
|
340
|
+
If market detail exposes stale LP token addresses, MCP now re-checks live pool token addresses from pool info before reporting balances.
|
|
337
341
|
|
|
338
342
|
---
|
|
339
343
|
|
|
@@ -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: [],
|
|
7
|
+
59141: [],
|
|
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.27)
|
|
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
|
|
|
@@ -42,6 +42,8 @@ You are an expert crypto trader using the MYX Protocol. To ensure successful exe
|
|
|
42
42
|
- **TP/SL Semantics**: LONG should use \`tpPrice > entryPrice\` and \`slPrice < entryPrice\`; SHORT uses the inverse. Plain integer strings like \`"65000"\` are treated as human prices, not implicit raw 30-decimal values.
|
|
43
43
|
- **LP Safety**: LP execution requires a fresh price snapshot and preview success; do not continue after preview failure.
|
|
44
44
|
- **LP Read Semantics**: Treat \`get_my_lp_holdings\` as an inventory listing, not a portfolio ranking by economic value.
|
|
45
|
+
- **LP Token Resolution**: If LP balances look empty but pool supply changed, re-check live pool token addresses from pool info instead of trusting market-detail LP token addresses blindly.
|
|
46
|
+
- **LP Approval Fallback**: Some test tokens revert on \`allowance()\`; MCP should treat that as a recoverable LP write-path issue and try the direct router approval/execution path.
|
|
45
47
|
|
|
46
48
|
## 3. Testnet Broker Reference
|
|
47
49
|
- Arbitrum test: \`0x895C4ae2A22bB26851011d733A9355f663a1F939\`
|
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.28" }, { capabilities: { tools: {}, resources: {}, prompts: {} } });
|
|
465
465
|
// List tools
|
|
466
466
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
467
467
|
return {
|
|
@@ -110,6 +110,14 @@ function isAbiLengthMismatchError(message) {
|
|
|
110
110
|
function isSdkWaitNotFunctionError(message) {
|
|
111
111
|
return message.toLowerCase().includes("wait is not a function");
|
|
112
112
|
}
|
|
113
|
+
function isAllowanceReadError(message) {
|
|
114
|
+
const lower = message.toLowerCase();
|
|
115
|
+
return (lower.includes("function \"allowance\" reverted") ||
|
|
116
|
+
lower.includes("function 'allowance' reverted") ||
|
|
117
|
+
lower.includes("allowance(address owner, address spender)") ||
|
|
118
|
+
lower.includes("allowance(address,address)") ||
|
|
119
|
+
lower.includes("allowance reverted"));
|
|
120
|
+
}
|
|
113
121
|
function readLastMockTxHash() {
|
|
114
122
|
const hash = String(globalThis?.__MCP_LAST_TX_HASH ?? "").trim();
|
|
115
123
|
if (!hash.startsWith("0x") || hash.length !== 66)
|
|
@@ -261,17 +269,33 @@ async function executeLiquidityTxViaRouter(params) {
|
|
|
261
269
|
throw new Error(`Liquidity ${poolType.toLowerCase()} ${action} amount must be > 0.`);
|
|
262
270
|
}
|
|
263
271
|
let approvalTxHash = null;
|
|
272
|
+
let approvalMode = "not_required";
|
|
273
|
+
let allowanceReadError = null;
|
|
264
274
|
if (action === "deposit") {
|
|
265
275
|
const tokenAddress = normalizeAddress(poolType === "QUOTE" ? marketDetail.quoteToken : marketDetail.baseToken, poolType === "QUOTE" ? "quoteToken" : "baseToken");
|
|
276
|
+
const approvalSpender = poolType === "QUOTE" ? addresses.quotePool : addresses.basePool;
|
|
266
277
|
const tokenContract = new Contract(tokenAddress, ERC20_ABI, signer);
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
278
|
+
let needsApproval = true;
|
|
279
|
+
try {
|
|
280
|
+
const allowance = toPositiveBigint(await tokenContract.allowance(address, approvalSpender)) ?? 0n;
|
|
281
|
+
needsApproval = allowance < amountIn;
|
|
282
|
+
approvalMode = needsApproval ? "checked_allowance_then_approve" : "existing_allowance";
|
|
283
|
+
if (!needsApproval) {
|
|
284
|
+
logger.info(`[LP fallback] allowance already sufficient for ${poolType} deposit. spender=${approvalSpender}, current=${allowance.toString()}, required=${amountIn.toString()}`);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
catch (error) {
|
|
288
|
+
allowanceReadError = extractErrorMessage(error);
|
|
289
|
+
approvalMode = "optimistic_approve_after_allowance_revert";
|
|
290
|
+
logger.warn(`[LP fallback] allowance read failed for ${poolType} deposit; trying direct approve on spender=${approvalSpender}. error=${allowanceReadError}`);
|
|
291
|
+
}
|
|
292
|
+
if (needsApproval) {
|
|
293
|
+
logger.info(`[LP fallback] approving spender=${approvalSpender} for ${poolType} deposit. required=${amountIn.toString()}, mode=${approvalMode}`);
|
|
294
|
+
const approveTx = await tokenContract.approve(approvalSpender, amountIn);
|
|
271
295
|
approvalTxHash = String(approveTx?.hash ?? "").trim() || null;
|
|
272
296
|
const approveReceipt = await approveTx?.wait?.();
|
|
273
297
|
if (approveReceipt && approveReceipt.status !== 1) {
|
|
274
|
-
throw new Error(`
|
|
298
|
+
throw new Error(`Approval transaction reverted for ${poolType} deposit (spender=${approvalSpender}).`);
|
|
275
299
|
}
|
|
276
300
|
}
|
|
277
301
|
}
|
|
@@ -363,6 +387,9 @@ async function executeLiquidityTxViaRouter(params) {
|
|
|
363
387
|
usedOraclePrice: oraclePayload.prices.length > 0,
|
|
364
388
|
oracleValue: oraclePayload.value.toString(),
|
|
365
389
|
approvalTxHash,
|
|
390
|
+
approvalMode,
|
|
391
|
+
allowanceReadError,
|
|
392
|
+
approvalSpender: action === "deposit" ? (poolType === "QUOTE" ? addresses.quotePool : addresses.basePool) : null,
|
|
366
393
|
},
|
|
367
394
|
};
|
|
368
395
|
}
|
|
@@ -398,6 +425,10 @@ async function executeLiquidityTx(params) {
|
|
|
398
425
|
logger.warn(`[LP SDK] ABI overload mismatch for ${poolType} ${action}; falling back to explicit router path.`);
|
|
399
426
|
return executeLiquidityTxViaRouter(params);
|
|
400
427
|
}
|
|
428
|
+
if (isAllowanceReadError(message)) {
|
|
429
|
+
logger.warn(`[LP SDK] allowance read failed for ${poolType} ${action}; falling back to explicit router path.`);
|
|
430
|
+
return executeLiquidityTxViaRouter(params);
|
|
431
|
+
}
|
|
401
432
|
throw error;
|
|
402
433
|
}
|
|
403
434
|
}
|
|
@@ -4,6 +4,7 @@ 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
6
|
import { getPoolList } from "../services/marketService.js";
|
|
7
|
+
import { getPoolInfo } from "../services/poolService.js";
|
|
7
8
|
const ADDRESS_RE = /^0x[a-fA-F0-9]{40}$/;
|
|
8
9
|
const ERC20_BALANCE_ABI = ["function balanceOf(address owner) view returns (uint256)"];
|
|
9
10
|
function collectRows(input) {
|
|
@@ -126,6 +127,40 @@ async function readErc20Balance(provider, tokenAddress, holder) {
|
|
|
126
127
|
const balance = await contract.balanceOf(holder);
|
|
127
128
|
return BigInt(balance).toString();
|
|
128
129
|
}
|
|
130
|
+
function collectAddressCandidates(...values) {
|
|
131
|
+
const unique = new Set();
|
|
132
|
+
for (const value of values) {
|
|
133
|
+
const normalized = readAddress(value);
|
|
134
|
+
if (normalized)
|
|
135
|
+
unique.add(normalized.toLowerCase());
|
|
136
|
+
}
|
|
137
|
+
return Array.from(unique.values());
|
|
138
|
+
}
|
|
139
|
+
async function resolveLpBalanceFromCandidates(provider, holder, candidateAddresses) {
|
|
140
|
+
let selectedAddress = null;
|
|
141
|
+
let selectedBalanceRaw = "0";
|
|
142
|
+
const warnings = [];
|
|
143
|
+
for (const candidate of candidateAddresses) {
|
|
144
|
+
try {
|
|
145
|
+
const balanceRaw = await readErc20Balance(provider, candidate, holder);
|
|
146
|
+
if (!selectedAddress) {
|
|
147
|
+
selectedAddress = candidate;
|
|
148
|
+
selectedBalanceRaw = balanceRaw;
|
|
149
|
+
}
|
|
150
|
+
if (BigInt(balanceRaw) > 0n) {
|
|
151
|
+
return { tokenAddress: candidate, balanceRaw, warnings };
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
warnings.push(`${candidate}: ${extractErrorMessage(error)}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
tokenAddress: selectedAddress,
|
|
160
|
+
balanceRaw: selectedBalanceRaw,
|
|
161
|
+
warnings,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
129
164
|
export const getMyLpHoldingsTool = {
|
|
130
165
|
name: "get_my_lp_holdings",
|
|
131
166
|
description: "[ACCOUNT] List your LP holdings across pools on the current wallet chain by reading base/quote LP token balances. Includes standardized LP asset names: base LP `mBASE.QUOTE`, quote LP `mQUOTE.BASE`.",
|
|
@@ -175,6 +210,7 @@ export const getMyLpHoldingsTool = {
|
|
|
175
210
|
let basePoolToken = readAddress(row?.basePoolToken ?? row?.base_pool_token);
|
|
176
211
|
let quotePoolToken = readAddress(row?.quotePoolToken ?? row?.quote_pool_token);
|
|
177
212
|
let detail = null;
|
|
213
|
+
let poolInfo = null;
|
|
178
214
|
if (!basePoolToken || !quotePoolToken) {
|
|
179
215
|
try {
|
|
180
216
|
const detailRes = await client.markets.getMarketDetail({ chainId, poolId });
|
|
@@ -188,23 +224,34 @@ export const getMyLpHoldingsTool = {
|
|
|
188
224
|
}
|
|
189
225
|
const { baseSymbol, quoteSymbol } = resolveBaseQuoteSymbols(row, detail);
|
|
190
226
|
const { baseLpAssetName, quoteLpAssetName } = buildLpAssetNames(baseSymbol, quoteSymbol);
|
|
191
|
-
let
|
|
192
|
-
let
|
|
193
|
-
|
|
227
|
+
let baseCandidates = collectAddressCandidates(basePoolToken);
|
|
228
|
+
let quoteCandidates = collectAddressCandidates(quotePoolToken);
|
|
229
|
+
let baseResolved = await resolveLpBalanceFromCandidates(provider, address, baseCandidates);
|
|
230
|
+
let quoteResolved = await resolveLpBalanceFromCandidates(provider, address, quoteCandidates);
|
|
231
|
+
const needPoolInfoEnrichment = (BigInt(baseResolved.balanceRaw) === 0n && BigInt(quoteResolved.balanceRaw) === 0n) ||
|
|
232
|
+
!baseResolved.tokenAddress ||
|
|
233
|
+
!quoteResolved.tokenAddress;
|
|
234
|
+
if (needPoolInfoEnrichment) {
|
|
194
235
|
try {
|
|
195
|
-
|
|
236
|
+
poolInfo = await getPoolInfo(poolId, chainId, client);
|
|
237
|
+
baseCandidates = collectAddressCandidates(...baseCandidates, poolInfo?.basePool?.poolToken, poolInfo?.basePool?.pool_token);
|
|
238
|
+
quoteCandidates = collectAddressCandidates(...quoteCandidates, poolInfo?.quotePool?.poolToken, poolInfo?.quotePool?.pool_token);
|
|
239
|
+
baseResolved = await resolveLpBalanceFromCandidates(provider, address, baseCandidates);
|
|
240
|
+
quoteResolved = await resolveLpBalanceFromCandidates(provider, address, quoteCandidates);
|
|
196
241
|
}
|
|
197
242
|
catch (error) {
|
|
198
|
-
warnings.push(`pool ${poolId}: failed to
|
|
243
|
+
warnings.push(`pool ${poolId}: failed to enrich pool info (${extractErrorMessage(error)})`);
|
|
199
244
|
}
|
|
200
245
|
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
246
|
+
const baseLpRaw = baseResolved.balanceRaw;
|
|
247
|
+
const quoteLpRaw = quoteResolved.balanceRaw;
|
|
248
|
+
basePoolToken = baseResolved.tokenAddress ?? basePoolToken;
|
|
249
|
+
quotePoolToken = quoteResolved.tokenAddress ?? quotePoolToken;
|
|
250
|
+
for (const warning of baseResolved.warnings) {
|
|
251
|
+
warnings.push(`pool ${poolId}: failed to read base LP balance (${warning})`);
|
|
252
|
+
}
|
|
253
|
+
for (const warning of quoteResolved.warnings) {
|
|
254
|
+
warnings.push(`pool ${poolId}: failed to read quote LP balance (${warning})`);
|
|
208
255
|
}
|
|
209
256
|
const hasAnyLp = BigInt(baseLpRaw) > 0n || BigInt(quoteLpRaw) > 0n;
|
|
210
257
|
if (!includeZero && !hasAnyLp)
|
package/dist/utils/slippage.js
CHANGED
|
@@ -3,7 +3,6 @@ export const SLIPPAGE_PCT_4DP_MAX = 10000n;
|
|
|
3
3
|
export const BUSINESS_SLIPPAGE_PCT_4DP_MAX = BigInt(process.env.BUSINESS_MAX_SLIPPAGE_PCT_4DP ?? "500");
|
|
4
4
|
export const SLIPPAGE_PCT_4DP_DESC = "Slippage in 4-decimal precision raw units (1 = 0.01%, 10000 = 100%)";
|
|
5
5
|
const SLIPPAGE_PERCENT_HUMAN_RE = /^\d+(\.\d{1,2})?$/;
|
|
6
|
-
export const BUSINESS_LP_SLIPPAGE_MAX_RATIO = Number(process.env.BUSINESS_MAX_LP_SLIPPAGE_RATIO ?? 0.05);
|
|
7
6
|
export function isValidSlippagePct4dp(value) {
|
|
8
7
|
if (!SLIPPAGE_PCT_4DP_RE.test(value))
|
|
9
8
|
return false;
|
|
@@ -50,9 +49,5 @@ export function normalizeLpSlippageRatio(value, label = "slippage") {
|
|
|
50
49
|
if (!Number.isFinite(numeric) || numeric < 0) {
|
|
51
50
|
throw new Error(`${label} must be a finite number >= 0.`);
|
|
52
51
|
}
|
|
53
|
-
|
|
54
|
-
if (ratio > BUSINESS_LP_SLIPPAGE_MAX_RATIO) {
|
|
55
|
-
throw new Error(`${label} exceeds business safety cap ${BUSINESS_LP_SLIPPAGE_MAX_RATIO * 100}%.`);
|
|
56
|
-
}
|
|
57
|
-
return ratio;
|
|
52
|
+
return numeric > 1 && numeric <= 100 ? numeric / 100 : numeric;
|
|
58
53
|
}
|