@michaleffffff/mcp-trading-server 3.0.20 → 3.0.24
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 +51 -1
- package/README.md +31 -7
- package/TOOL_EXAMPLES.md +25 -2
- package/dist/auth/resolveClient.js +3 -3
- package/dist/prompts/tradingGuide.js +15 -7
- package/dist/server.js +2 -2
- package/dist/services/marketService.js +41 -0
- package/dist/services/poolService.js +45 -84
- package/dist/services/tradeService.js +61 -6
- package/dist/tools/accountTransfer.js +14 -7
- package/dist/tools/closeAllPositions.js +30 -23
- package/dist/tools/closePosition.js +15 -1
- package/dist/tools/executeTrade.js +27 -2
- package/dist/tools/getMyLpHoldings.js +4 -8
- package/dist/tools/getPoolMetadata.js +11 -19
- package/dist/tools/manageLiquidity.js +7 -2
- package/dist/tools/manageTpSl.js +81 -7
- package/dist/tools/openPositionSimple.js +37 -18
- package/dist/utils/mappings.js +2 -2
- package/dist/utils/slippage.js +20 -0
- package/dist/utils/token.js +15 -0
- package/dist/utils/units.js +4 -12
- package/dist/utils/verification.js +43 -12
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,12 +1,62 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 3.0.
|
|
3
|
+
## 3.0.24 - 2026-03-19
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- Hardened LP MCP behavior:
|
|
7
|
+
- `get_pool_metadata(includeLiquidity=true)` now ignores caller-supplied `marketPrice` and derives liquidity depth from a fresh Oracle price only.
|
|
8
|
+
- `get_pool_info` MCP-side market price resolution no longer falls back from fresh Oracle to ticker-derived price.
|
|
9
|
+
- Beta LP router / `PoolManager` fallback mappings were completed for MCP-managed LP and create-market paths.
|
|
10
|
+
- `get_my_lp_holdings` no longer ranks rows by mixed BASE/QUOTE LP raw balances.
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- Synced release metadata and operator docs for `v3.0.24`:
|
|
14
|
+
- Updated `README.md`, `TOOL_EXAMPLES.md`, and `trading_best_practices` prompt version strings.
|
|
15
|
+
- Refreshed LP safety docs to reflect Oracle-only liquidity metadata and inventory-only LP holdings semantics.
|
|
16
|
+
|
|
17
|
+
## 3.0.23 - 2026-03-19
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
- Upgraded SDK dependency to `@myx-trade/sdk@^1.0.4-beta.4`.
|
|
21
|
+
- Refreshed operator-facing docs and runtime hints to match the new SDK baseline:
|
|
22
|
+
- `README.md` now references `@myx-trade/sdk@^1.0.4-beta.4`.
|
|
23
|
+
- `TOOL_EXAMPLES.md`, `execute_trade`, `close_position`, and `mapTimeInForce` now document the current IOC-only `timeInForce` behavior using `v1.0.4-beta.4`.
|
|
24
|
+
- `trading_best_practices` prompt now aligns with release `v3.0.23`.
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
- Hardened LP MCP behavior and synchronized docs:
|
|
28
|
+
- `get_pool_metadata(includeLiquidity=true)` now ignores caller-supplied `marketPrice` and derives liquidity depth from a fresh Oracle price only.
|
|
29
|
+
- `get_pool_info` MCP-side price resolution no longer falls back from fresh Oracle to ticker-derived market price.
|
|
30
|
+
- Beta LP router / `PoolManager` chain mappings were completed for the MCP fallback path.
|
|
31
|
+
- `get_my_lp_holdings` no longer ranks rows by mixed BASE/QUOTE LP raw balances.
|
|
32
|
+
- `README.md`, `TOOL_EXAMPLES.md`, and `mcp_config_guide.md` now document these LP-specific MCP semantics.
|
|
33
|
+
|
|
34
|
+
## 3.0.22 - 2026-03-19
|
|
35
|
+
|
|
36
|
+
### Changed
|
|
37
|
+
- Upgraded SDK dependency to `@myx-trade/sdk@^1.0.4-beta.1`.
|
|
38
|
+
- Refreshed operator-facing docs and runtime hints to match the new SDK baseline:
|
|
39
|
+
- `README.md` now references `@myx-trade/sdk@^1.0.4-beta.1`.
|
|
40
|
+
- `TOOL_EXAMPLES.md`, `execute_trade`, `close_position`, and `mapTimeInForce` now document the current IOC-only `timeInForce` behavior using the new SDK version.
|
|
41
|
+
- `trading_best_practices` prompt now aligns with release `v3.0.22`.
|
|
42
|
+
|
|
43
|
+
## 3.0.21 - 2026-03-19
|
|
4
44
|
|
|
5
45
|
### Fixed
|
|
6
46
|
- Hardened `get_pool_metadata` funding-rate formatting:
|
|
7
47
|
- `fundingInfo.nextFundingRate` now preserves readable `%/秒` and `%/天` output for negative rates too.
|
|
8
48
|
- Regression coverage now asserts both numeric funding-rate views and display strings.
|
|
9
49
|
|
|
50
|
+
### Changed
|
|
51
|
+
- Refreshed operator-facing docs and prompts to match the latest trading safety behavior:
|
|
52
|
+
- `README.md` now documents Oracle-only execution, exact-approval defaults, base-size semantics, TP/SL semantic checks, and LP preview fail-close.
|
|
53
|
+
- `mcp_config_guide.md` now includes required `BROKER_ADDRESS` configuration and testnet `MYXBroker` references.
|
|
54
|
+
- `TOOL_EXAMPLES.md` now reflects fresh-Oracle execution, live-direction validation, human-price TP/SL parsing, and trading `slippagePct` conventions.
|
|
55
|
+
- `trading_best_practices` prompt now aligns with the current MCP safety constraints and testnet broker references.
|
|
56
|
+
- Updated testnet broker defaults:
|
|
57
|
+
- Arbitrum Sepolia `BROKER_ADDRESS` -> `0x895C4ae2A22bB26851011d733A9355f663a1F939`
|
|
58
|
+
- Linea Sepolia `BROKER_ADDRESS` -> `0x634EfDC9dC76D7AbF6E49279875a31B02E9891e2`
|
|
59
|
+
|
|
10
60
|
## 3.0.19 - 2026-03-19
|
|
11
61
|
|
|
12
62
|
### Fixed
|
package/README.md
CHANGED
|
@@ -6,10 +6,11 @@ A production-ready MCP (Model Context Protocol) server for deep integration with
|
|
|
6
6
|
|
|
7
7
|
# Release Notes
|
|
8
8
|
|
|
9
|
-
- **Current release: 3.0.
|
|
10
|
-
- **SDK baseline**: `@myx-trade/sdk@^1.0.
|
|
9
|
+
- **Current release: 3.0.24**
|
|
10
|
+
- **SDK baseline**: `@myx-trade/sdk@^1.0.4-beta.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
|
+
- **Safety refresh**: Docs and prompt guidance now reflect Oracle-only execution, exact-approval defaults, notional-based fee checks, TP/SL semantic validation, and LP preview fail-close behavior.
|
|
13
14
|
- **Breaking changes**: Many low-level tools (e.g., `get_market_price`, `get_oracle_price`, `get_open_orders`) have been merged into unified counterparts.
|
|
14
15
|
|
|
15
16
|
---
|
|
@@ -31,13 +32,20 @@ Copy `.env.example` to `.env` and configure your trading wallet:
|
|
|
31
32
|
|
|
32
33
|
```bash
|
|
33
34
|
PRIVATE_KEY=0x...
|
|
34
|
-
RPC_URL=https://
|
|
35
|
-
CHAIN_ID
|
|
35
|
+
RPC_URL=https://your-testnet-or-mainnet-rpc
|
|
36
|
+
CHAIN_ID=...
|
|
36
37
|
BROKER_ADDRESS=0x...
|
|
37
38
|
QUOTE_TOKEN_ADDRESS=0x...
|
|
38
|
-
QUOTE_TOKEN_DECIMALS
|
|
39
|
+
QUOTE_TOKEN_DECIMALS=...
|
|
39
40
|
```
|
|
40
41
|
|
|
42
|
+
## Testnet `MYXBroker` Reference
|
|
43
|
+
|
|
44
|
+
- Arbitrum test: `0x895C4ae2A22bB26851011d733A9355f663a1F939`
|
|
45
|
+
- Linea test: `0x634EfDC9dC76D7AbF6E49279875a31B02E9891e2`
|
|
46
|
+
|
|
47
|
+
Use the broker that matches your active RPC and chain configuration.
|
|
48
|
+
|
|
41
49
|
---
|
|
42
50
|
|
|
43
51
|
# Core Tools Reference
|
|
@@ -47,7 +55,7 @@ QUOTE_TOKEN_DECIMALS=18
|
|
|
47
55
|
* **`list_pools`**: List all tradable assets on the current chain.
|
|
48
56
|
* **`search_tools`**: Discover the right MCP tool by keyword, legacy tool name, or intent phrase.
|
|
49
57
|
* **`get_price`**: Fetch real-time prices (Impact Market Price or Oracle Price).
|
|
50
|
-
* **`get_pool_metadata`**: Comprehensive metrics (Fees, Open Interest, Liquidity Depth).
|
|
58
|
+
* **`get_pool_metadata`**: Comprehensive metrics (Fees, Open Interest, Liquidity Depth via fresh Oracle price).
|
|
51
59
|
* **`get_kline`**: Fetch candlestick data for technical analysis.
|
|
52
60
|
|
|
53
61
|
### ⚔️ Trading Operations
|
|
@@ -77,6 +85,19 @@ QUOTE_TOKEN_DECIMALS=18
|
|
|
77
85
|
|
|
78
86
|
---
|
|
79
87
|
|
|
88
|
+
# Safety Defaults
|
|
89
|
+
|
|
90
|
+
- **Oracle-only execution**: trading paths now require a fresh Oracle price and reject stale / missing execution prices.
|
|
91
|
+
- **Exact approvals by default**: local fallback flows now prefer exact approvals instead of implicit unlimited approval.
|
|
92
|
+
- **Size semantics**: `size` always means base-asset quantity, not USD notional.
|
|
93
|
+
- **Direction validation**: when a tool operates on an existing `positionId`, the supplied `direction` must match the live position.
|
|
94
|
+
- **TP/SL semantics**: LONG requires `tpPrice > entryPrice` and `slPrice < entryPrice`; SHORT uses the inverse.
|
|
95
|
+
- **LP safety**: LP preview failures are fail-close and no longer downgrade to `minAmountOut=0`.
|
|
96
|
+
- **LP metadata safety**: `get_pool_metadata(includeLiquidity=true)` now ignores caller-supplied `marketPrice` and derives liquidity depth from a fresh Oracle price only.
|
|
97
|
+
- **LP holdings semantics**: `get_my_lp_holdings` is an inventory view; returned rows are no longer ranked by mixed BASE/QUOTE LP raw balances.
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
80
101
|
# Tool Discovery
|
|
81
102
|
|
|
82
103
|
When a client or LLM is unsure which tool to call:
|
|
@@ -108,9 +129,10 @@ Use these conventions when generating tool arguments:
|
|
|
108
129
|
- `status`: `OPEN` / `HISTORY` / `ALL` are canonical; lowercase is tolerated
|
|
109
130
|
- `poolType`: `BASE` / `QUOTE` are canonical; lowercase is tolerated
|
|
110
131
|
- `orderType`: `MARKET` / `LIMIT` / `STOP` / `CONDITIONAL`
|
|
111
|
-
- `timeInForce`: SDK `v1.0.
|
|
132
|
+
- `timeInForce`: SDK `v1.0.4-beta.4` currently supports `IOC` only, so use `0` or `"IOC"`
|
|
112
133
|
- `size`: base token quantity, not USD notional; expected order value is usually `collateralAmount * leverage`
|
|
113
134
|
- `executionFeeToken`: must be a real token address; zero address is rejected. Use the pool `quoteToken`
|
|
135
|
+
- `slippagePct`: trading tools use 4-decimal raw units where `100 = 1.00%` and `50 = 0.50%`
|
|
114
136
|
- Human units: `"100"` means 100 USDC or 100 token units depending on field
|
|
115
137
|
- Raw units: `"raw:1000000"` means exact on-chain integer units
|
|
116
138
|
|
|
@@ -136,6 +158,8 @@ Examples:
|
|
|
136
158
|
}
|
|
137
159
|
```
|
|
138
160
|
|
|
161
|
+
For `get_pool_metadata(includeLiquidity=true)`, do not rely on a custom `marketPrice` override. MCP now ignores that field and uses a fresh Oracle price automatically.
|
|
162
|
+
|
|
139
163
|
```json
|
|
140
164
|
{
|
|
141
165
|
"name": "open_position_simple",
|
package/TOOL_EXAMPLES.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# MYX MCP Tool Examples Handbook (v3.0.
|
|
1
|
+
# MYX MCP Tool Examples Handbook (v3.0.24)
|
|
2
2
|
|
|
3
3
|
This guide provides practical MCP payload examples for the current unified toolset.
|
|
4
4
|
All examples use the MCP format:
|
|
@@ -7,6 +7,13 @@ All examples use the MCP format:
|
|
|
7
7
|
{ "name": "tool_name", "arguments": { "...": "..." } }
|
|
8
8
|
```
|
|
9
9
|
|
|
10
|
+
## Environment Notes
|
|
11
|
+
|
|
12
|
+
- Common testnet brokers:
|
|
13
|
+
- Arbitrum test: `0x895C4ae2A22bB26851011d733A9355f663a1F939`
|
|
14
|
+
- Linea test: `0x634EfDC9dC76D7AbF6E49279875a31B02E9891e2`
|
|
15
|
+
- Keep `RPC_URL`, `CHAIN_ID`, and `BROKER_ADDRESS` on the same network.
|
|
16
|
+
|
|
10
17
|
---
|
|
11
18
|
|
|
12
19
|
## Discovery First
|
|
@@ -75,6 +82,8 @@ Recommended high-level entry tool.
|
|
|
75
82
|
`marketId` is optional on `open_position_simple`. If supplied, it is validated against the market resolved from `poolId` or `keyword`.
|
|
76
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...`.
|
|
77
84
|
`collateralAmount` remains required on `open_position_simple`; if omitted, MCP now returns an actionable suggestion instead of a generic parse error.
|
|
85
|
+
If `price` is omitted, the tool uses a fresh Oracle price only; stale or unresolved execution prices are rejected.
|
|
86
|
+
Auto-computed `tradingFee` follows notional semantics rather than raw collateral-only estimation.
|
|
78
87
|
|
|
79
88
|
Raw-units example:
|
|
80
89
|
|
|
@@ -111,8 +120,9 @@ Low-level increase-order tool when you want full control.
|
|
|
111
120
|
}
|
|
112
121
|
```
|
|
113
122
|
|
|
114
|
-
`timeInForce` should be `0` (or `"IOC"` in string form) for SDK `v1.0.
|
|
123
|
+
`timeInForce` should be `0` (or `"IOC"` in string form) for SDK `v1.0.4-beta.4`.
|
|
115
124
|
`executionFeeToken` must be a real token address; do not pass the zero address. Use the pool `quoteToken`.
|
|
125
|
+
If `positionId` is supplied on increase flows, `direction` must remain consistent with the live position.
|
|
116
126
|
|
|
117
127
|
### `close_position`
|
|
118
128
|
Close or reduce a position. Use `ALL` for a full close.
|
|
@@ -137,6 +147,8 @@ Close or reduce a position. Use `ALL` for a full close.
|
|
|
137
147
|
}
|
|
138
148
|
```
|
|
139
149
|
|
|
150
|
+
`direction` must match the live position. MCP now validates live direction before sending the close request.
|
|
151
|
+
|
|
140
152
|
### `manage_tp_sl`
|
|
141
153
|
Create or update TP/SL on an open position.
|
|
142
154
|
|
|
@@ -154,6 +166,9 @@ Create or update TP/SL on an open position.
|
|
|
154
166
|
}
|
|
155
167
|
```
|
|
156
168
|
|
|
169
|
+
Plain integer prices such as `"2800"` are treated as human prices, not implicit raw 30-decimal values.
|
|
170
|
+
For LONG positions, use `tpPrice > entryPrice` and `slPrice < entryPrice`. For SHORT positions, use the inverse.
|
|
171
|
+
|
|
157
172
|
Delete both TP/SL orders:
|
|
158
173
|
|
|
159
174
|
```json
|
|
@@ -234,6 +249,7 @@ Unified pool detail, config, and liquidity info.
|
|
|
234
249
|
```
|
|
235
250
|
|
|
236
251
|
`get_pool_metadata` returns raw values in `poolInfo` and precision-safe human-readable values in `poolInfoFormatted`, including readable funding epoch timestamps, funding-rate `%/秒` and `%/天`, and IO notional-at-entry views.
|
|
252
|
+
When `includeLiquidity=true`, MCP derives liquidity depth from a fresh Oracle price automatically. Any caller-supplied `marketPrice` is now treated as deprecated and ignored.
|
|
237
253
|
|
|
238
254
|
### `get_kline`
|
|
239
255
|
Read chart data. Use `limit: 1` for the latest bar.
|
|
@@ -284,6 +300,9 @@ Alias-friendly form also works:
|
|
|
284
300
|
}
|
|
285
301
|
```
|
|
286
302
|
|
|
303
|
+
LP preview failures now fail closed; the server no longer downgrades to `minAmountOut=0`.
|
|
304
|
+
Oracle-backed LP pricing requires a fresh price snapshot before execution.
|
|
305
|
+
|
|
287
306
|
### `get_lp_price`
|
|
288
307
|
Read LP NAV price for BASE or QUOTE side.
|
|
289
308
|
|
|
@@ -312,6 +331,8 @@ Read current LP balances across pools.
|
|
|
312
331
|
}
|
|
313
332
|
```
|
|
314
333
|
|
|
334
|
+
`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.
|
|
335
|
+
|
|
315
336
|
---
|
|
316
337
|
|
|
317
338
|
## Account And Monitoring
|
|
@@ -391,3 +412,5 @@ Read open positions, history, or both.
|
|
|
391
412
|
4. Canonical enums are still preferred:
|
|
392
413
|
`OPEN|HISTORY|ALL`, `BASE|QUOTE`, `LONG|SHORT`, `MARKET|LIMIT|STOP`.
|
|
393
414
|
5. The server tolerates common lowercase and alias forms for better AI compatibility.
|
|
415
|
+
6. Trading `slippagePct` uses 4-decimal raw units, so `100 = 1.00%` and `50 = 0.50%`.
|
|
416
|
+
7. High-risk execution paths prefer fresh Oracle pricing, exact approval sizing, and fail-close behavior on missing previews or invalid units.
|
|
@@ -5,10 +5,10 @@ let cached = null;
|
|
|
5
5
|
function getDefaultBrokerByChainId(chainId) {
|
|
6
6
|
// Testnet mappings
|
|
7
7
|
if (chainId === 421614)
|
|
8
|
-
return "
|
|
8
|
+
return "0x895C4ae2A22bB26851011d733A9355f663a1F939"; // Arbitrum Sepolia
|
|
9
9
|
if (chainId === 59141)
|
|
10
|
-
return "
|
|
11
|
-
return "
|
|
10
|
+
return "0x634EfDC9dC76D7AbF6E49279875a31B02E9891e2"; // Linea Sepolia
|
|
11
|
+
return "0x895C4ae2A22bB26851011d733A9355f663a1F939";
|
|
12
12
|
}
|
|
13
13
|
function getDefaultQuoteTokenByChainId(chainId) {
|
|
14
14
|
// 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.24)
|
|
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
|
|
|
@@ -28,21 +28,29 @@ You are an expert crypto trader using the MYX Protocol. To ensure successful exe
|
|
|
28
28
|
- **Consolidated Tools**: Many legacy tools have been merged. Always use the high-level versions (e.g., \`get_price\` instead of \`get_market_price\`).
|
|
29
29
|
- **Discovery**: \`search_tools\` understands legacy names like \`get_open_orders\` and intent phrases like \`add base lp\`.
|
|
30
30
|
- **Unit Prefixes**: Prefer \`human:\` for readable amounts (e.g., "100" USDC) and \`raw:\` for exact on-chain units.
|
|
31
|
-
- **Slippage**:
|
|
31
|
+
- **Slippage**: Trading tools use 4-decimal raw units where \`100 = 1.00%\` and \`50 = 0.50%\`. Keep it tight unless the market is genuinely illiquid.
|
|
32
32
|
- **Fees**: Use \`get_pool_metadata\` to view current fee tiers and pool configuration.
|
|
33
|
+
- **Liquidity Metadata**: When calling \`get_pool_metadata(includeLiquidity=true)\`, MCP uses a fresh Oracle price automatically and ignores caller-supplied \`marketPrice\`.
|
|
33
34
|
- **LP Strategy**: Use \`get_my_lp_holdings\` to monitor liquidity positions. Naming follows \`mBASE.QUOTE\` (e.g., \`mBTC.USDC\`).
|
|
34
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
|
+
- **Oracle Safety**: Execution flows now require a fresh Oracle price. Do not fall back to stale ticker or user-supplied execution prices when the Oracle is unavailable.
|
|
37
|
+
- **Approval Safety**: Local fallback flows prefer exact approval sizing. Do not assume unlimited approvals are necessary.
|
|
38
|
+
- **Position Semantics**: \`size\` is BASE quantity, not USD notional. If a \`positionId\` is supplied, \`direction\` must match the live position.
|
|
39
|
+
- **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.
|
|
40
|
+
- **LP Safety**: LP execution requires a fresh price snapshot and preview success; do not continue after preview failure.
|
|
41
|
+
- **LP Read Semantics**: Treat \`get_my_lp_holdings\` as an inventory listing, not a portfolio ranking by economic value.
|
|
42
|
+
|
|
43
|
+
## 3. Testnet Broker Reference
|
|
44
|
+
- Arbitrum test: \`0x895C4ae2A22bB26851011d733A9355f663a1F939\`
|
|
45
|
+
- Linea test: \`0x634EfDC9dC76D7AbF6E49279875a31B02E9891e2\`
|
|
46
|
+
- Always keep \`RPC_URL\`, \`CHAIN_ID\`, and \`BROKER_ADDRESS\` on the same network.
|
|
35
47
|
|
|
36
48
|
Current Session:
|
|
37
49
|
- Wallet: ${address}
|
|
38
50
|
- Chain ID: ${chainId}
|
|
39
51
|
|
|
40
|
-
##
|
|
52
|
+
## 4. Self-Healing
|
|
41
53
|
If a transaction reverts with a hex code, the server will attempt to decode it (e.g., "AccountInsufficientFreeAmount"). Error payloads now include structured \`code/hint/action\` fields; use them to provide concrete next steps.
|
|
42
|
-
|
|
43
|
-
Current Session:
|
|
44
|
-
- Wallet: ${address}
|
|
45
|
-
- Chain ID: ${chainId}
|
|
46
54
|
`
|
|
47
55
|
}
|
|
48
56
|
}
|
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.24" }, { capabilities: { tools: {}, resources: {}, prompts: {} } });
|
|
465
465
|
// List tools
|
|
466
466
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
467
467
|
return {
|
|
@@ -582,7 +582,7 @@ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
|
582
582
|
async function main() {
|
|
583
583
|
const transport = new StdioServerTransport();
|
|
584
584
|
await server.connect(transport);
|
|
585
|
-
logger.info("🚀 MYX Trading MCP Server v3.0.
|
|
585
|
+
logger.info("🚀 MYX Trading MCP Server v3.0.24 running (stdio, pure on-chain, prod ready)");
|
|
586
586
|
}
|
|
587
587
|
main().catch((err) => {
|
|
588
588
|
logger.error("Fatal Server Startup Error", err);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { getChainId } from "../auth/resolveClient.js";
|
|
2
2
|
import { getMarketStateDesc } from "../utils/mappings.js";
|
|
3
|
+
export const DEFAULT_ORACLE_MAX_AGE_SEC = Number(process.env.ORACLE_MAX_AGE_SEC ?? 90);
|
|
3
4
|
function collectRows(input) {
|
|
4
5
|
if (Array.isArray(input))
|
|
5
6
|
return input.flatMap(collectRows);
|
|
@@ -92,6 +93,46 @@ export async function getOraclePrice(client, poolId, chainIdOverride) {
|
|
|
92
93
|
const chainId = chainIdOverride ?? getChainId();
|
|
93
94
|
return client.utils.getOraclePrice(poolId, chainId);
|
|
94
95
|
}
|
|
96
|
+
function parseOraclePublishTime(value, poolId) {
|
|
97
|
+
const text = String(value ?? "").trim();
|
|
98
|
+
if (!/^\d+$/.test(text)) {
|
|
99
|
+
throw new Error(`Oracle publishTime missing or invalid for poolId=${poolId}.`);
|
|
100
|
+
}
|
|
101
|
+
const parsed = BigInt(text);
|
|
102
|
+
if (parsed <= 0n) {
|
|
103
|
+
throw new Error(`Oracle publishTime must be positive for poolId=${poolId}.`);
|
|
104
|
+
}
|
|
105
|
+
return parsed;
|
|
106
|
+
}
|
|
107
|
+
export function assertOracleFreshness(publishTimeValue, poolId, maxAgeSec = DEFAULT_ORACLE_MAX_AGE_SEC) {
|
|
108
|
+
if (!Number.isFinite(maxAgeSec) || maxAgeSec <= 0) {
|
|
109
|
+
throw new Error(`Invalid oracle max age configuration: ${maxAgeSec}`);
|
|
110
|
+
}
|
|
111
|
+
const publishTime = parseOraclePublishTime(publishTimeValue, poolId);
|
|
112
|
+
const nowSec = BigInt(Math.floor(Date.now() / 1000));
|
|
113
|
+
const maxAge = BigInt(Math.floor(maxAgeSec));
|
|
114
|
+
if (publishTime > nowSec + 5n) {
|
|
115
|
+
throw new Error(`Oracle publishTime is in the future for poolId=${poolId}.`);
|
|
116
|
+
}
|
|
117
|
+
const age = nowSec - publishTime;
|
|
118
|
+
if (age > maxAge) {
|
|
119
|
+
throw new Error(`Oracle price expired for poolId=${poolId}: age=${age.toString()}s exceeds maxAge=${maxAge.toString()}s.`);
|
|
120
|
+
}
|
|
121
|
+
return publishTime;
|
|
122
|
+
}
|
|
123
|
+
export async function getFreshOraclePrice(client, poolId, chainIdOverride, maxAgeSec = DEFAULT_ORACLE_MAX_AGE_SEC) {
|
|
124
|
+
const oracle = await getOraclePrice(client, poolId, chainIdOverride);
|
|
125
|
+
const price = String(oracle?.price ?? "").trim();
|
|
126
|
+
if (!price) {
|
|
127
|
+
throw new Error(`Oracle price missing for poolId=${poolId}.`);
|
|
128
|
+
}
|
|
129
|
+
const publishTime = assertOracleFreshness(oracle?.publishTime, poolId, maxAgeSec);
|
|
130
|
+
return {
|
|
131
|
+
...oracle,
|
|
132
|
+
price,
|
|
133
|
+
publishTime: publishTime.toString(),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
95
136
|
export async function searchMarket(client, keyword, limit = 1000, chainIdOverride) {
|
|
96
137
|
const chainId = chainIdOverride ?? getChainId();
|
|
97
138
|
const normalizedKeyword = String(keyword ?? "").trim();
|
|
@@ -1,13 +1,23 @@
|
|
|
1
1
|
import { pool, quote, base } from "@myx-trade/sdk";
|
|
2
2
|
import { getChainId, resolveClient } from "../auth/resolveClient.js";
|
|
3
3
|
import { extractErrorMessage } from "../utils/errorMessage.js";
|
|
4
|
-
import { ensureUnits } from "../utils/units.js";
|
|
4
|
+
import { ensureUnits, parseUserUnits } from "../utils/units.js";
|
|
5
5
|
import { normalizeAddress } from "../utils/address.js";
|
|
6
|
-
import { Contract
|
|
6
|
+
import { Contract } from "ethers";
|
|
7
7
|
import { logger } from "../utils/logger.js";
|
|
8
|
+
import { assertOracleFreshness, getFreshOraclePrice } from "./marketService.js";
|
|
8
9
|
const LP_DECIMALS = 18;
|
|
9
10
|
const POOL_MANAGER_BY_CHAIN = {
|
|
10
11
|
421614: "0xf268D9FeD3Bd56fd9aBdb4FeEb993338613678A8",
|
|
12
|
+
59141: "0x85e869d98216221807A06636541Ec93C9c0a4B0c",
|
|
13
|
+
97: "0x4F917ef137b573D9790b87e3cF6dfb698cF00c9c",
|
|
14
|
+
56: "0x13F2130c2F3bfd612BBCBF35FB9E467dd32bAF3A",
|
|
15
|
+
};
|
|
16
|
+
const POOL_MANAGER_BY_CHAIN_BETA = {
|
|
17
|
+
421614: "0x05314a21Fc97B74f168730153b2B63A870D25dE5",
|
|
18
|
+
59141: "0xcf51a6895864c6D8E507fC31EF16b9011287c5f4",
|
|
19
|
+
97: "0x9E84a999e15CCdb2F64a5AF10939c25769dF6b07",
|
|
20
|
+
56: "0x9E84a999e15CCdb2F64a5AF10939c25769dF6b07",
|
|
11
21
|
};
|
|
12
22
|
const POOL_MANAGER_ABI = [
|
|
13
23
|
"function deployPool((bytes32 marketId,address baseToken))",
|
|
@@ -51,6 +61,11 @@ const LP_ROUTER_BY_CHAIN_BETA = {
|
|
|
51
61
|
basePool: "0x51B62554a76197d5DF2D5dC4D57FF54d40775938",
|
|
52
62
|
quotePool: "0x783Ed065a12e1C1D33c2a8d6408385C1843D3084",
|
|
53
63
|
},
|
|
64
|
+
56: {
|
|
65
|
+
router: "0x0F4C6f18Fb136DD1eBd6Da3C5d86a86597CF79a3",
|
|
66
|
+
basePool: "0x51B62554a76197d5DF2D5dC4D57FF54d40775938",
|
|
67
|
+
quotePool: "0x783Ed065a12e1C1D33c2a8d6408385C1843D3084",
|
|
68
|
+
},
|
|
54
69
|
};
|
|
55
70
|
const PREVIEW_POOL_ABI = [
|
|
56
71
|
"function previewLpAmountOut(bytes32,uint256,uint256) view returns (uint256)",
|
|
@@ -150,9 +165,10 @@ function getPoolManagerAddress(chainId) {
|
|
|
150
165
|
if (envAddress) {
|
|
151
166
|
return normalizeAddress(envAddress, "POOL_MANAGER_ADDRESS");
|
|
152
167
|
}
|
|
153
|
-
const
|
|
168
|
+
const source = isBetaModeEnabled() ? POOL_MANAGER_BY_CHAIN_BETA : POOL_MANAGER_BY_CHAIN;
|
|
169
|
+
const mapped = source[chainId];
|
|
154
170
|
if (!mapped) {
|
|
155
|
-
throw new Error(`Pool manager address is not configured for chainId=${chainId}. Set POOL_MANAGER_ADDRESS env var.`);
|
|
171
|
+
throw new Error(`Pool manager address is not configured for chainId=${chainId} (beta=${isBetaModeEnabled()}). Set POOL_MANAGER_ADDRESS env var.`);
|
|
156
172
|
}
|
|
157
173
|
return mapped;
|
|
158
174
|
}
|
|
@@ -202,10 +218,13 @@ async function buildOraclePricePayload(client, chainId, poolId, fallbackOracleTy
|
|
|
202
218
|
if (!vaa || !vaa.startsWith("0x")) {
|
|
203
219
|
throw new Error(`Oracle VAA unavailable for pool ${poolId}.`);
|
|
204
220
|
}
|
|
205
|
-
const publishTime =
|
|
221
|
+
const publishTime = assertOracleFreshness(oracle?.publishTime, poolId);
|
|
206
222
|
const oracleType = Number.isFinite(Number(oracle?.oracleType)) ? Number(oracle.oracleType) : fallbackOracleType;
|
|
207
223
|
const value = toPositiveBigint(oracle?.value) ?? 0n;
|
|
208
224
|
const referencePrice30 = BigInt(ensureUnits(String(oracle?.price ?? "0"), 30, "oracle price"));
|
|
225
|
+
if (referencePrice30 <= 0n) {
|
|
226
|
+
throw new Error(`Oracle price must be positive for pool ${poolId}.`);
|
|
227
|
+
}
|
|
209
228
|
return {
|
|
210
229
|
prices: [[poolId, oracleType, publishTime, vaa]],
|
|
211
230
|
value,
|
|
@@ -229,8 +248,7 @@ async function previewAmountOutForLiquidity(signer, chainId, poolId, poolType, a
|
|
|
229
248
|
return toPositiveBigint(out) ?? 0n;
|
|
230
249
|
}
|
|
231
250
|
catch (error) {
|
|
232
|
-
|
|
233
|
-
return 0n;
|
|
251
|
+
throw new Error(`LP preview failed: ${extractErrorMessage(error)}`);
|
|
234
252
|
}
|
|
235
253
|
}
|
|
236
254
|
async function executeLiquidityTxViaRouter(params) {
|
|
@@ -244,7 +262,7 @@ async function executeLiquidityTxViaRouter(params) {
|
|
|
244
262
|
if (!Number.isFinite(decimals) || decimals < 0) {
|
|
245
263
|
throw new Error(`Invalid decimals while preparing ${poolType} ${action} transaction.`);
|
|
246
264
|
}
|
|
247
|
-
const amountIn =
|
|
265
|
+
const amountIn = BigInt(parseUserUnits(String(amount), decimals, "amount"));
|
|
248
266
|
if (amountIn <= 0n) {
|
|
249
267
|
throw new Error(`Liquidity ${poolType.toLowerCase()} ${action} amount must be > 0.`);
|
|
250
268
|
}
|
|
@@ -255,7 +273,7 @@ async function executeLiquidityTxViaRouter(params) {
|
|
|
255
273
|
const allowance = toPositiveBigint(await tokenContract.allowance(address, addresses.router)) ?? 0n;
|
|
256
274
|
if (allowance < amountIn) {
|
|
257
275
|
logger.info(`[LP fallback] allowance insufficient for ${poolType} deposit, approving router. required=${amountIn.toString()}, current=${allowance.toString()}`);
|
|
258
|
-
const approveTx = await tokenContract.approve(addresses.router,
|
|
276
|
+
const approveTx = await tokenContract.approve(addresses.router, amountIn);
|
|
259
277
|
approvalTxHash = String(approveTx?.hash ?? "").trim() || null;
|
|
260
278
|
const approveReceipt = await approveTx?.wait?.();
|
|
261
279
|
if (approveReceipt && approveReceipt.status !== 1) {
|
|
@@ -350,28 +368,14 @@ async function resolvePositiveMarketPrice30(client, poolId, chainId) {
|
|
|
350
368
|
if (!client)
|
|
351
369
|
return null;
|
|
352
370
|
try {
|
|
353
|
-
const oracle = await client
|
|
354
|
-
const
|
|
355
|
-
const byPrice = toPositiveBigint(
|
|
356
|
-
if (byValue)
|
|
357
|
-
return byValue;
|
|
371
|
+
const oracle = await getFreshOraclePrice(client, poolId, chainId);
|
|
372
|
+
const oraclePrice30 = ensureUnits(String(oracle?.price ?? "").trim(), 30, "oracle price");
|
|
373
|
+
const byPrice = toPositiveBigint(oraclePrice30);
|
|
358
374
|
if (byPrice)
|
|
359
375
|
return byPrice;
|
|
360
376
|
}
|
|
361
377
|
catch {
|
|
362
378
|
}
|
|
363
|
-
try {
|
|
364
|
-
const tickerRes = await client.markets?.getTickerList?.({ chainId, poolIds: [poolId] });
|
|
365
|
-
const row = Array.isArray(tickerRes) ? tickerRes[0] : tickerRes?.data?.[0];
|
|
366
|
-
if (row?.price) {
|
|
367
|
-
const tickerRaw = ensureUnits(row.price, 30, "ticker price");
|
|
368
|
-
const byTicker = toPositiveBigint(tickerRaw);
|
|
369
|
-
if (byTicker)
|
|
370
|
-
return byTicker;
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
catch {
|
|
374
|
-
}
|
|
375
379
|
return null;
|
|
376
380
|
}
|
|
377
381
|
/**
|
|
@@ -472,89 +476,46 @@ export async function getPoolDetail(poolId, chainIdOverride) {
|
|
|
472
476
|
/**
|
|
473
477
|
* 获取流动性信息
|
|
474
478
|
*/
|
|
475
|
-
export async function getLiquidityInfo(client, poolId,
|
|
479
|
+
export async function getLiquidityInfo(client, poolId, chainIdOverride) {
|
|
476
480
|
const chainId = chainIdOverride ?? getChainId();
|
|
477
|
-
|
|
481
|
+
const oracle = await getFreshOraclePrice(client, poolId, chainId);
|
|
482
|
+
const marketPrice = ensureUnits(String(oracle?.price ?? "").trim(), 30, "oracle price");
|
|
483
|
+
const liquidityInfo = await client.utils.getLiquidityInfo({ chainId, poolId, marketPrice });
|
|
484
|
+
return {
|
|
485
|
+
liquidityInfo,
|
|
486
|
+
marketPrice,
|
|
487
|
+
marketPriceSource: "oracle",
|
|
488
|
+
oraclePublishTime: String(oracle?.publishTime ?? ""),
|
|
489
|
+
oracleType: oracle?.oracleType ?? null,
|
|
490
|
+
};
|
|
478
491
|
}
|
|
479
492
|
/**
|
|
480
493
|
* Quote 池 deposit
|
|
481
494
|
*/
|
|
482
495
|
export async function quoteDeposit(poolId, amount, slippage, chainIdOverride) {
|
|
483
496
|
const chainId = chainIdOverride ?? getChainId();
|
|
484
|
-
|
|
485
|
-
try {
|
|
486
|
-
return await withMutedSdkAbiMismatchLogs(() => quote.deposit({ chainId, poolId, amount, slippage }));
|
|
487
|
-
}
|
|
488
|
-
catch (error) {
|
|
489
|
-
const message = extractErrorMessage(error);
|
|
490
|
-
const recovered = recoverSdkSubmittedTxHash(txHashBefore, message, { poolType: "QUOTE", action: "deposit" });
|
|
491
|
-
if (recovered)
|
|
492
|
-
return recovered;
|
|
493
|
-
if (!isAbiLengthMismatchError(message))
|
|
494
|
-
throw error;
|
|
495
|
-
logger.warn("quote.deposit hit SDK ABI mismatch; switching to explicit router path.");
|
|
496
|
-
return executeLiquidityTxViaRouter({ chainId, poolId, poolType: "QUOTE", action: "deposit", amount, slippage });
|
|
497
|
-
}
|
|
497
|
+
return executeLiquidityTxViaRouter({ chainId, poolId, poolType: "QUOTE", action: "deposit", amount, slippage });
|
|
498
498
|
}
|
|
499
499
|
/**
|
|
500
500
|
* Quote 池 withdraw
|
|
501
501
|
*/
|
|
502
502
|
export async function quoteWithdraw(poolId, amount, slippage, chainIdOverride) {
|
|
503
503
|
const chainId = chainIdOverride ?? getChainId();
|
|
504
|
-
|
|
505
|
-
try {
|
|
506
|
-
return await withMutedSdkAbiMismatchLogs(() => quote.withdraw({ chainId, poolId, amount, slippage }));
|
|
507
|
-
}
|
|
508
|
-
catch (error) {
|
|
509
|
-
const message = extractErrorMessage(error);
|
|
510
|
-
const recovered = recoverSdkSubmittedTxHash(txHashBefore, message, { poolType: "QUOTE", action: "withdraw" });
|
|
511
|
-
if (recovered)
|
|
512
|
-
return recovered;
|
|
513
|
-
if (!isAbiLengthMismatchError(message))
|
|
514
|
-
throw error;
|
|
515
|
-
logger.warn("quote.withdraw hit SDK ABI mismatch; switching to explicit router path.");
|
|
516
|
-
return executeLiquidityTxViaRouter({ chainId, poolId, poolType: "QUOTE", action: "withdraw", amount, slippage });
|
|
517
|
-
}
|
|
504
|
+
return executeLiquidityTxViaRouter({ chainId, poolId, poolType: "QUOTE", action: "withdraw", amount, slippage });
|
|
518
505
|
}
|
|
519
506
|
/**
|
|
520
507
|
* Base 池 deposit
|
|
521
508
|
*/
|
|
522
509
|
export async function baseDeposit(poolId, amount, slippage, chainIdOverride) {
|
|
523
510
|
const chainId = chainIdOverride ?? getChainId();
|
|
524
|
-
|
|
525
|
-
try {
|
|
526
|
-
return await withMutedSdkAbiMismatchLogs(() => base.deposit({ chainId, poolId, amount, slippage }));
|
|
527
|
-
}
|
|
528
|
-
catch (error) {
|
|
529
|
-
const message = extractErrorMessage(error);
|
|
530
|
-
const recovered = recoverSdkSubmittedTxHash(txHashBefore, message, { poolType: "BASE", action: "deposit" });
|
|
531
|
-
if (recovered)
|
|
532
|
-
return recovered;
|
|
533
|
-
if (!isAbiLengthMismatchError(message))
|
|
534
|
-
throw error;
|
|
535
|
-
logger.warn("base.deposit hit SDK ABI mismatch; switching to explicit router path.");
|
|
536
|
-
return executeLiquidityTxViaRouter({ chainId, poolId, poolType: "BASE", action: "deposit", amount, slippage });
|
|
537
|
-
}
|
|
511
|
+
return executeLiquidityTxViaRouter({ chainId, poolId, poolType: "BASE", action: "deposit", amount, slippage });
|
|
538
512
|
}
|
|
539
513
|
/**
|
|
540
514
|
* Base 池 withdraw
|
|
541
515
|
*/
|
|
542
516
|
export async function baseWithdraw(poolId, amount, slippage, chainIdOverride) {
|
|
543
517
|
const chainId = chainIdOverride ?? getChainId();
|
|
544
|
-
|
|
545
|
-
try {
|
|
546
|
-
return await withMutedSdkAbiMismatchLogs(() => base.withdraw({ chainId, poolId, amount, slippage }));
|
|
547
|
-
}
|
|
548
|
-
catch (error) {
|
|
549
|
-
const message = extractErrorMessage(error);
|
|
550
|
-
const recovered = recoverSdkSubmittedTxHash(txHashBefore, message, { poolType: "BASE", action: "withdraw" });
|
|
551
|
-
if (recovered)
|
|
552
|
-
return recovered;
|
|
553
|
-
if (!isAbiLengthMismatchError(message))
|
|
554
|
-
throw error;
|
|
555
|
-
logger.warn("base.withdraw hit SDK ABI mismatch; switching to explicit router path.");
|
|
556
|
-
return executeLiquidityTxViaRouter({ chainId, poolId, poolType: "BASE", action: "withdraw", amount, slippage });
|
|
557
|
-
}
|
|
518
|
+
return executeLiquidityTxViaRouter({ chainId, poolId, poolType: "BASE", action: "withdraw", amount, slippage });
|
|
558
519
|
}
|
|
559
520
|
/**
|
|
560
521
|
* 获取 LP 价格
|