@michaleffffff/mcp-trading-server 3.0.1 → 3.0.3

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,24 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.0.3 - 2026-03-17
4
+
5
+ ### Changed
6
+ - Enhanced `create_perp_market` tool description to better explain `marketId` requirements.
7
+ - Added usage hints to `execute_trade` schema for improved AI coordination.
8
+ - Internal: `resolvePool` now supports `chainIdOverride` for more flexible market resolution.
9
+
10
+ ## 3.0.2 - 2026-03-17
11
+
12
+ ### Added
13
+ - Integrated `verifyTradeOutcome` in `execute_trade` for post-transaction state validation.
14
+ - Automatic `tradingFee` calculation in `execute_trade` using `getUserTradingFeeRate`.
15
+ - `parseUserUnits` / `parseUserPrice30` utilities for more consistent human/raw input handling.
16
+
17
+ ### Changed
18
+ - `execute_trade` now surfaces `preflight` normalization details in the successful response.
19
+ - `adjust_margin` now supports displaying `__normalized` data.
20
+ - Refined unit resolution in `account_deposit` and `account_withdraw`.
21
+
3
22
  ## 3.0.1 - 2026-03-17
4
23
 
5
24
  ### Added
package/README.md CHANGED
@@ -124,7 +124,7 @@ The server will run using `stdio` transport and can be connected by any MCP-comp
124
124
  2. **Configure env**: Copy `.env.example` to `.env` and fill in `PRIVATE_KEY`, `RPC_URL`, etc.
125
125
  3. **Start the server**: Run `npm run build` then `npm run start` (use `npm run dev` for development).
126
126
  4. **Configure your MCP client**: Set `command/args/env` in your MCP client (see Claude Desktop example below).
127
- 5. **Common flow**: Use `search_market` (or `get_market_list`) to get `poolId`, then optionally `get_market_price` / `get_oracle_price` to view prices, and finally use `open_position_simple` (recommended) to open a position.
127
+ 5. **Common flow**: Use `search_market` (or `get_pool_list`) to get `poolId`, then optionally `get_market_price` / `get_oracle_price` to view prices, and finally use `open_position_simple` (recommended) to open a position.
128
128
  6. **Tool examples**: See `TOOL_EXAMPLES.md` for practical examples for every tool, common workflows, and parameter-unit guidance.
129
129
 
130
130
  **Minimal open position example:**
@@ -200,7 +200,7 @@ Common error codes:
200
200
  Search behavior in P0:
201
201
  - `search_market` now accepts empty `keyword` and returns active markets.
202
202
  - When `search_market` returns empty/unstable data, server-side fallback uses SDK `api.getMarketList()` and `api.getPoolList()` to rebuild active-market results.
203
- - For robust discovery, you can also call `get_market_list` directly.
203
+ - For robust discovery, you can also call `get_pool_list` directly.
204
204
 
205
205
  Example:
206
206
 
@@ -285,13 +285,13 @@ Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
285
285
 
286
286
  # Tools
287
287
 
288
- The server exposes 39 tools categorized for AI:
288
+ The server exposes tools categorized for AI:
289
289
 
290
290
  For practical tool-by-tool examples, see: `TOOL_EXAMPLES.md`
291
291
 
292
292
  ### Trading Operations
293
293
  * **open_position_simple**: High-level open position helper (recommended). Computes price/size/tradingFee internally.
294
- * **execute_trade**: Execute a new trade or add to an existing position.
294
+ * **execute_trade**: Execute a new trade or add to an existing position (includes parameter preflight, supports `human:`/`raw:` amount prefixes, auto-computes `tradingFee` when omitted).
295
295
  * **close_position**: Close an open position.
296
296
  * **close_all_positions**: Emergency: close ALL open positions in a pool at once.
297
297
  * **cancel_order**: Cancel an open order by its order ID.
@@ -308,12 +308,13 @@ For practical tool-by-tool examples, see: `TOOL_EXAMPLES.md`
308
308
  * **get_oracle_price**: Get the current EVM oracle price for a specific pool.
309
309
  * **get_kline_latest_bar**: Get only the latest bar for an interval.
310
310
  * **get_all_tickers**: Get all ticker snapshots in one request.
311
- * **get_market_list**, **get_market_list_raw**, **get_kline**, **get_pool_info**... (`get_pool_info` auto-retries with oracle/ticker price on divide-by-zero reads)
311
+ * **get_kline**, **get_pool_info**... (`get_pool_info` auto-retries with oracle/ticker price on divide-by-zero reads)
312
312
  * **get_pool_symbol_all**, **get_base_detail**, **get_pool_level_config**...
313
313
 
314
314
  ### Account & Portfolio
315
315
  * **get_positions**: Get all open positions for the current account.
316
316
  * **get_account**: Unified account snapshot. Clearly separates **wallet balance** and **trading-account balance** (provide `poolId` for full trading-account metrics).
317
+ * **get_my_lp_holdings**: Scan pools and return your base/quote LP token balances on the current chain, with standardized LP asset names (`base: mBASE.QUOTE`, `quote: mQUOTE.BASE`).
317
318
  * **get_account_vip_info**: Query account VIP tier details.
318
319
  * **get_position_history**, **get_open_orders**, **get_order_history**...
319
320
 
@@ -323,7 +324,8 @@ For practical tool-by-tool examples, see: `TOOL_EXAMPLES.md`
323
324
  - If one section fails (wallet or trading-account), the tool may return a **partial** snapshot with `meta.partial=true` and section-level `error` details.
324
325
 
325
326
  ### Liquidity Provision (LP)
326
- * **manage_liquidity**: Add or withdraw liquidity from a BASE or QUOTE pool (aliases: `add/remove/increase/decrease` are supported).
327
+ * **manage_liquidity**: Add or withdraw liquidity from a BASE or QUOTE pool (aliases: `add/remove/increase/decrease` are supported). `poolId` is pre-validated on target `chainId` for clearer errors. Response includes LP naming metadata (`baseLpAssetName=mBASE.QUOTE`, `quoteLpAssetName=mQUOTE.BASE`, `operatedLpAssetName`).
328
+ - Example (`BTC/USDC`, `poolType=QUOTE`): `operatedLpAssetName = mUSDC.BTC`.
327
329
  * **create_perp_market**: Create a new perpetual trading pair.
328
330
 
329
331
  ---
@@ -377,6 +379,12 @@ Use this query/mutation loop for limit-order management:
377
379
  4. Check historical fills/cancellations with `get_order_history`.
378
380
  5. Cancel one order via `cancel_order` or batch via `cancel_all_orders`.
379
381
 
382
+ ## Unit Safety (P0)
383
+
384
+ - Monetary fields in major mutation tools support `human:` and `raw:` prefixes.
385
+ - Default behavior is human-readable units (e.g. `1` means `1.0` token), while `raw:<int>` enforces exact raw units.
386
+ - `execute_trade` and `adjust_margin` responses include normalized raw values for auditability.
387
+
380
388
  Minimal query examples:
381
389
 
382
390
  ```json
package/TOOL_EXAMPLES.md CHANGED
@@ -80,11 +80,14 @@ It includes:
80
80
  "slippagePct": "100",
81
81
  "executionFeeToken": "0xQUOTE_TOKEN",
82
82
  "leverage": 5,
83
- "tradingFee": "raw:100000",
84
83
  "marketId": "0xMARKET_ID"
85
84
  }
86
85
  }
87
86
  ```
87
+ Notes:
88
+ - `tradingFee` is optional (auto-computed if omitted).
89
+ - Amount-like fields support `human:` and `raw:` prefixes.
90
+ - `positionId: "0x000...000"` is auto-treated as a new position.
88
91
 
89
92
  ### `close_position`
90
93
  ```json
@@ -234,6 +237,7 @@ Raw precision mode:
234
237
  }
235
238
  }
236
239
  ```
240
+ Success payload includes normalized raw amount under `data.normalized.adjustAmountRaw`.
237
241
 
238
242
  ### `check_approval`
239
243
  ```json
@@ -292,24 +296,6 @@ Raw precision mode:
292
296
  ```
293
297
  Note: this tool may internally fallback to SDK `api.getMarketList()` / `api.getPoolList()` when direct search is empty.
294
298
 
295
- ### `get_market_list`
296
- ```json
297
- {
298
- "name": "get_market_list",
299
- "arguments": {
300
- "limit": 50
301
- }
302
- }
303
- ```
304
-
305
- ### `get_market_list_raw`
306
- ```json
307
- {
308
- "name": "get_market_list_raw",
309
- "arguments": {}
310
- }
311
- ```
312
-
313
299
  ### `get_pool_list`
314
300
  ```json
315
301
  {
@@ -451,6 +437,31 @@ If one section fails, check `data.meta.partial` and section-level `error` fields
451
437
  }
452
438
  ```
453
439
 
440
+ ### `get_my_lp_holdings`
441
+ ```json
442
+ {
443
+ "name": "get_my_lp_holdings",
444
+ "arguments": {}
445
+ }
446
+ ```
447
+ LP asset naming rule in each item:
448
+ - `baseLpAssetName`: `mBASE.QUOTE` (e.g. `mBTC.USDC`)
449
+ - `quoteLpAssetName`: `mQUOTE.BASE` (e.g. `mUSDC.BTC`)
450
+ Include zero-balance pools and optional filtering:
451
+ ```json
452
+ {
453
+ "name": "get_my_lp_holdings",
454
+ "arguments": {
455
+ "includeZero": true,
456
+ "poolIds": [
457
+ "0xPOOL_ID_1",
458
+ "0xPOOL_ID_2"
459
+ ],
460
+ "maxPools": 50
461
+ }
462
+ }
463
+ ```
464
+
454
465
  ### `get_positions`
455
466
  ```json
456
467
  {
@@ -559,6 +570,31 @@ Alias action example (`remove` == `withdraw`):
559
570
  }
560
571
  ```
561
572
  If operation fails, check `error.message` for concrete reasons (for example `Insufficient Balance`) instead of generic `undefined`.
573
+ `poolId` must be valid on the target `chainId`; if uncertain, resolve with `search_market` / `get_pool_list` first.
574
+ Success response includes `data.lpAssetNames`:
575
+ - `baseLpAssetName`: `mBASE.QUOTE` (e.g. `mBTC.USDC`)
576
+ - `quoteLpAssetName`: `mQUOTE.BASE` (e.g. `mUSDC.BTC`)
577
+ - `operatedLpAssetName`: LP asset name matching current `poolType`
578
+ Example success payload (abridged):
579
+ ```json
580
+ {
581
+ "status": "success",
582
+ "data": {
583
+ "confirmation": {
584
+ "confirmed": true,
585
+ "txHash": "0x..."
586
+ },
587
+ "lpAssetNames": {
588
+ "baseSymbol": "BTC",
589
+ "quoteSymbol": "USDC",
590
+ "baseLpAssetName": "mBTC.USDC",
591
+ "quoteLpAssetName": "mUSDC.BTC",
592
+ "operatedPoolType": "QUOTE",
593
+ "operatedLpAssetName": "mUSDC.BTC"
594
+ }
595
+ }
596
+ }
597
+ ```
562
598
 
563
599
  ### `get_lp_price`
564
600
  ```json
@@ -17,7 +17,7 @@ export const tradingGuidePrompt = {
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
 
19
19
  ## 1. The Standard Workflow
20
- 1. **Discovery**: Use \`search_market\` with a keyword (or empty keyword) to find active \`poolId\` values. If needed, fallback to \`get_market_list\`.
20
+ 1. **Discovery**: Use \`search_market\` with a keyword (or empty keyword) to find active \`poolId\` values. If needed, fallback to \`get_pool_list\`.
21
21
  2. **Context**: Use \`get_market_price\` and \`get_account\` (with \`poolId\`) to check market state, wallet balance, and trading-account margin.
22
22
  3. **Execution**: Prefer \`open_position_simple\` for new trades. It handles unit conversions and pool resolution automatically.
23
23
  4. **Validation**: Always check the \`verification.verified\` flag in the output. If \`false\`, read the \`cancelReason\` to explain the failure to the user.
@@ -26,9 +26,13 @@ You are an expert crypto trader using the MYX Protocol. To ensure successful exe
26
26
  ## 2. Parameter Tips
27
27
  - **Position IDs**: When opening a NEW position, \`positionId\` MUST be an empty string \`""\`.
28
28
  - **Decimals**: Human-readable units (e.g., "0.1" BTC) are default for \`open_position_simple\`. SDK-native tools often require raw units; use the \`raw:\` prefix if you need forced precision.
29
+ - **Unit Prefixes**: For mutation tools, prefer explicit prefixes: \`human:\` for readable amounts, \`raw:\` for exact on-chain units.
29
30
  - **Slippage**: Default is 100 (1%). For volatile meme tokens, consider 200-300 (2-3%).
30
31
  - **Fees**: Use \`get_user_trading_fee_rate\` to estimate fees before large trades.
32
+ - **execute_trade**: Has built-in preflight normalization and can auto-compute \`tradingFee\` when omitted.
31
33
  - **Balances**: Use \`get_account\` to clearly separate wallet balance vs trading-account balance (pass \`poolId\` for trading-account metrics).
34
+ - **LP Holdings**: Use \`get_my_lp_holdings\` to scan base/quote LP token balances across pools on the current chain; naming convention is \`baseLpAssetName=mBASE.QUOTE\`, \`quoteLpAssetName=mQUOTE.BASE\` (e.g., \`mBTC.USDC\`, \`mUSDC.BTC\`).
35
+ - **Liquidity Mutation Output**: \`manage_liquidity\` success output includes \`data.lpAssetNames\` with \`baseLpAssetName\`, \`quoteLpAssetName\`, and \`operatedLpAssetName\`.
32
36
  - **Adjust Margin**: \`adjust_margin.adjustAmount\` supports human amount (e.g. "1"), and also supports exact raw amount via \`raw:\` prefix.
33
37
  - **TP/SL Updates**: \`update_order_tp_sl\` accepts boolean-like values for \`useOrderCollateral\` and \`isTpSlOrder\`, but pending LIMIT/STOP orders can still be rejected by protocol rules; after fill, prefer \`set_tp_sl\`.
34
38
  - **Pool Reads**: \`get_pool_info\` auto-retries with oracle/ticker price when direct on-chain read returns divide-by-zero.
package/dist/server.js CHANGED
@@ -103,6 +103,102 @@ function parseFirstText(result) {
103
103
  const firstText = result.content.find((item) => item?.type === "text" && typeof item.text === "string");
104
104
  return firstText?.text ?? null;
105
105
  }
106
+ function unwrapSchema(schema) {
107
+ let current = schema;
108
+ for (let i = 0; i < 8; i++) {
109
+ const type = current?._def?.type;
110
+ if (!type)
111
+ break;
112
+ if (type === "optional" || type === "nullable" || type === "default" || type === "prefault" || type === "catch") {
113
+ current = current?._def?.innerType;
114
+ continue;
115
+ }
116
+ if (type === "pipe") {
117
+ current = current?._def?.out;
118
+ continue;
119
+ }
120
+ break;
121
+ }
122
+ return current;
123
+ }
124
+ function coerceBooleanString(input) {
125
+ if (typeof input !== "string")
126
+ return input;
127
+ const normalized = input.trim().toLowerCase();
128
+ if (normalized === "true" || normalized === "1")
129
+ return true;
130
+ if (normalized === "false" || normalized === "0")
131
+ return false;
132
+ return input;
133
+ }
134
+ function coerceStringToStringArray(input) {
135
+ if (Array.isArray(input))
136
+ return input.map((item) => String(item).trim()).filter(Boolean);
137
+ if (typeof input !== "string")
138
+ return input;
139
+ const text = input.trim();
140
+ if (!text)
141
+ return input;
142
+ if (text.startsWith("[") && text.endsWith("]")) {
143
+ try {
144
+ const parsed = JSON.parse(text);
145
+ if (Array.isArray(parsed)) {
146
+ return parsed.map((item) => String(item).trim()).filter(Boolean);
147
+ }
148
+ }
149
+ catch {
150
+ }
151
+ }
152
+ if (text.includes(",")) {
153
+ const split = text.split(",").map((item) => item.trim()).filter(Boolean);
154
+ if (split.length > 0)
155
+ return split;
156
+ }
157
+ return input;
158
+ }
159
+ function coerceBySchema(value, schema) {
160
+ const unwrapped = unwrapSchema(schema);
161
+ const type = unwrapped?._def?.type;
162
+ if (type === "boolean") {
163
+ return coerceBooleanString(value);
164
+ }
165
+ if (type === "array") {
166
+ const elementType = unwrapSchema(unwrapped?._def?.element)?._def?.type;
167
+ if (elementType === "string") {
168
+ return coerceStringToStringArray(value);
169
+ }
170
+ }
171
+ if (type === "union") {
172
+ const options = Array.isArray(unwrapped?._def?.options) ? unwrapped._def.options : [];
173
+ for (const option of options) {
174
+ const optionType = unwrapSchema(option)?._def?.type;
175
+ if (optionType === "boolean") {
176
+ const coerced = coerceBooleanString(value);
177
+ if (typeof coerced === "boolean")
178
+ return coerced;
179
+ }
180
+ if (optionType === "array") {
181
+ const coerced = coerceStringToStringArray(value);
182
+ if (Array.isArray(coerced))
183
+ return coerced;
184
+ }
185
+ }
186
+ }
187
+ return value;
188
+ }
189
+ function normalizeToolArgsBySchema(rawArgs, schema) {
190
+ const source = rawArgs && typeof rawArgs === "object" ? { ...rawArgs } : {};
191
+ const normalized = {};
192
+ for (const key of Object.keys(source)) {
193
+ normalized[key] = source[key];
194
+ }
195
+ for (const [key, fieldSchema] of Object.entries(schema)) {
196
+ if (!Object.prototype.hasOwnProperty.call(source, key))
197
+ continue;
198
+ normalized[key] = coerceBySchema(source[key], fieldSchema);
199
+ }
200
+ return normalized;
201
+ }
106
202
  function normalizeToolErrorResult(toolName, result) {
107
203
  const rawText = parseFirstText(result);
108
204
  if (!rawText) {
@@ -208,27 +304,30 @@ function normalizeSdkReadFailure(toolName, result) {
208
304
  function zodSchemaToJsonSchema(zodSchema) {
209
305
  const toPropSchema = (value) => {
210
306
  const def = value?._def;
211
- const typeName = def?.typeName;
212
- if (typeName === "ZodOptional" || typeName === "ZodNullable") {
307
+ const zodType = def?.type;
308
+ if (zodType === "optional" || zodType === "nullable" || zodType === "default" || zodType === "prefault" || zodType === "catch") {
213
309
  return toPropSchema(def?.innerType);
214
310
  }
215
- if (typeName === "ZodString")
311
+ if (zodType === "pipe") {
312
+ return toPropSchema(def?.out);
313
+ }
314
+ if (zodType === "string")
216
315
  return { type: "string" };
217
- if (typeName === "ZodNumber")
316
+ if (zodType === "number")
218
317
  return { type: "number" };
219
- if (typeName === "ZodBoolean")
318
+ if (zodType === "boolean")
220
319
  return { type: "boolean" };
221
- if (typeName === "ZodArray") {
222
- return { type: "array", items: toPropSchema(def?.type) };
320
+ if (zodType === "array") {
321
+ return { type: "array", items: toPropSchema(def?.element) };
223
322
  }
224
- if (typeName === "ZodLiteral") {
323
+ if (zodType === "literal") {
225
324
  return { const: def?.value };
226
325
  }
227
- if (typeName === "ZodUnion") {
326
+ if (zodType === "union") {
228
327
  const options = def?.options || [];
229
328
  return { anyOf: options.map((opt) => toPropSchema(opt)) };
230
329
  }
231
- if (typeName === "ZodObject") {
330
+ if (zodType === "object") {
232
331
  return { type: "object" };
233
332
  }
234
333
  return { type: "string" };
@@ -237,7 +336,8 @@ function zodSchemaToJsonSchema(zodSchema) {
237
336
  const required = [];
238
337
  for (const [key, value] of Object.entries(zodSchema)) {
239
338
  const desc = value?._def?.description || "";
240
- const isOptional = value?.isOptional?.() || value?._def?.typeName === "ZodOptional";
339
+ const fieldType = value?._def?.type;
340
+ const isOptional = value?.isOptional?.() || fieldType === "optional" || fieldType === "default";
241
341
  const prop = toPropSchema(value);
242
342
  if (desc)
243
343
  prop.description = desc;
@@ -252,7 +352,7 @@ function zodSchemaToJsonSchema(zodSchema) {
252
352
  };
253
353
  }
254
354
  // ─── MCP Server ───
255
- const server = new Server({ name: "myx-mcp-trading-server", version: "3.0.1" }, { capabilities: { tools: {}, resources: {}, prompts: {} } });
355
+ const server = new Server({ name: "myx-mcp-trading-server", version: "3.0.3" }, { capabilities: { tools: {}, resources: {}, prompts: {} } });
256
356
  // List tools
257
357
  server.setRequestHandler(ListToolsRequestSchema, async () => {
258
358
  return {
@@ -279,8 +379,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
279
379
  try {
280
380
  let validatedArgs = args ?? {};
281
381
  if (tool.schema) {
382
+ const normalizedArgs = normalizeToolArgsBySchema(args ?? {}, tool.schema);
282
383
  try {
283
- validatedArgs = z.object(tool.schema).strict().parse(args ?? {});
384
+ validatedArgs = z.object(tool.schema).strict().parse(normalizedArgs);
284
385
  }
285
386
  catch (validationError) {
286
387
  if (validationError instanceof ZodError) {
@@ -372,7 +473,7 @@ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
372
473
  async function main() {
373
474
  const transport = new StdioServerTransport();
374
475
  await server.connect(transport);
375
- logger.info("🚀 MYX Trading MCP Server v3.0.1 running (stdio, pure on-chain, prod ready)");
476
+ logger.info("🚀 MYX Trading MCP Server v3.0.3 running (stdio, pure on-chain, prod ready)");
376
477
  }
377
478
  main().catch((err) => {
378
479
  logger.error("Fatal Server Startup Error", err);
@@ -162,8 +162,8 @@ export async function getPoolLevelConfig(client, poolId, chainIdOverride) {
162
162
  /**
163
163
  * 智能解析 Pool ID (支持 ID 校验与关键词回退)
164
164
  */
165
- export async function resolvePool(client, poolId, keyword) {
166
- const chainId = getChainId();
165
+ export async function resolvePool(client, poolId, keyword, chainIdOverride) {
166
+ const chainId = chainIdOverride ?? getChainId();
167
167
  let pid = poolId?.trim();
168
168
  // 1. 如果提供了 poolId,先尝试验证其是否存在
169
169
  if (pid) {
@@ -136,7 +136,7 @@ export async function openPosition(client, address, args) {
136
136
  }
137
137
  const baseDecimals = poolData.baseDecimals || 18;
138
138
  const quoteDecimals = poolData.quoteDecimals || 6;
139
- const collateralRaw = ensureUnits(args.collateralAmount, quoteDecimals, "collateralAmount");
139
+ const collateralRaw = ensureUnits(args.collateralAmount, quoteDecimals, "collateralAmount", { allowImplicitRaw: false });
140
140
  // --- Pre-flight Check: minOrderSizeInUsd ---
141
141
  try {
142
142
  const levelRes = await client.markets.getPoolLevelConfig(args.poolId, chainId);
@@ -163,9 +163,9 @@ export async function openPosition(client, address, args) {
163
163
  throw e;
164
164
  console.warn(`[tradeService] Limit check skipped: ${e.message}`);
165
165
  }
166
- const sizeRaw = ensureUnits(args.size, baseDecimals, "size");
167
- const priceRaw = ensureUnits(args.price, 30, "price");
168
- const tradingFeeRaw = ensureUnits(args.tradingFee, quoteDecimals, "tradingFee");
166
+ const sizeRaw = ensureUnits(args.size, baseDecimals, "size", { allowImplicitRaw: false });
167
+ const priceRaw = ensureUnits(args.price, 30, "price", { allowImplicitRaw: false });
168
+ const tradingFeeRaw = ensureUnits(args.tradingFee, quoteDecimals, "tradingFee", { allowImplicitRaw: false });
169
169
  // --- Auto-Deposit Logic (Strict) ---
170
170
  const allowAutoDeposit = args.autoDeposit !== false;
171
171
  console.log(`[tradeService] Checking marginBalance for ${address} in pool ${args.poolId}...`);
@@ -227,13 +227,13 @@ export async function openPosition(client, address, args) {
227
227
  leverage: args.leverage,
228
228
  };
229
229
  if (args.tpSize)
230
- orderParams.tpSize = ensureUnits(args.tpSize, baseDecimals, "tpSize");
230
+ orderParams.tpSize = ensureUnits(args.tpSize, baseDecimals, "tpSize", { allowImplicitRaw: false });
231
231
  if (args.tpPrice)
232
- orderParams.tpPrice = ensureUnits(args.tpPrice, 30, "tpPrice");
232
+ orderParams.tpPrice = ensureUnits(args.tpPrice, 30, "tpPrice", { allowImplicitRaw: false });
233
233
  if (args.slSize)
234
- orderParams.slSize = ensureUnits(args.slSize, baseDecimals, "slSize");
234
+ orderParams.slSize = ensureUnits(args.slSize, baseDecimals, "slSize", { allowImplicitRaw: false });
235
235
  if (args.slPrice)
236
- orderParams.slPrice = ensureUnits(args.slPrice, 30, "slPrice");
236
+ orderParams.slPrice = ensureUnits(args.slPrice, 30, "slPrice", { allowImplicitRaw: false });
237
237
  return client.order.createIncreaseOrder(orderParams, tradingFeeRaw, args.marketId);
238
238
  }
239
239
  /**
@@ -263,9 +263,9 @@ export async function closePosition(client, address, args) {
263
263
  orderType: args.orderType,
264
264
  triggerType: resolveTriggerType(args.orderType, args.direction, true, args.triggerType),
265
265
  direction: dir,
266
- collateralAmount: ensureUnits(args.collateralAmount, quoteDecimals, "collateralAmount"),
267
- size: ensureUnits(args.size, baseDecimals, "size"),
268
- price: ensureUnits(args.price, 30, "price"),
266
+ collateralAmount: ensureUnits(args.collateralAmount, quoteDecimals, "collateralAmount", { allowImplicitRaw: false }),
267
+ size: ensureUnits(args.size, baseDecimals, "size", { allowImplicitRaw: false }),
268
+ price: ensureUnits(args.price, 30, "price", { allowImplicitRaw: false }),
269
269
  timeInForce: args.timeInForce,
270
270
  postOnly: args.postOnly,
271
271
  slippagePct: normalizeSlippagePct4dp(args.slippagePct),
@@ -303,13 +303,13 @@ export async function setPositionTpSl(client, address, args) {
303
303
  slippagePct: normalizeSlippagePct4dp(args.slippagePct),
304
304
  };
305
305
  if (args.tpPrice)
306
- params.tpPrice = ensureUnits(args.tpPrice, 30, "tpPrice");
306
+ params.tpPrice = ensureUnits(args.tpPrice, 30, "tpPrice", { allowImplicitRaw: false });
307
307
  if (args.tpSize)
308
- params.tpSize = ensureUnits(args.tpSize, baseDecimals, "tpSize");
308
+ params.tpSize = ensureUnits(args.tpSize, baseDecimals, "tpSize", { allowImplicitRaw: false });
309
309
  if (args.slPrice)
310
- params.slPrice = ensureUnits(args.slPrice, 30, "slPrice");
310
+ params.slPrice = ensureUnits(args.slPrice, 30, "slPrice", { allowImplicitRaw: false });
311
311
  if (args.slSize)
312
- params.slSize = ensureUnits(args.slSize, baseDecimals, "slSize");
312
+ params.slSize = ensureUnits(args.slSize, baseDecimals, "slSize", { allowImplicitRaw: false });
313
313
  return client.order.createPositionTpSlOrder(params);
314
314
  }
315
315
  /**
@@ -334,7 +334,7 @@ export async function adjustMargin(client, address, args) {
334
334
  catch {
335
335
  // Fallback to env quote decimals if market detail is unavailable.
336
336
  }
337
- const adjustAmount = ensureUnits(adjustAmountInput, quoteDecimals, "adjustAmount");
337
+ const adjustAmount = ensureUnits(adjustAmountInput, quoteDecimals, "adjustAmount", { allowImplicitRaw: false });
338
338
  if (!/^-?\d+$/.test(adjustAmount)) {
339
339
  throw new Error("adjustAmount must resolve to an integer string (raw units).");
340
340
  }
@@ -349,7 +349,14 @@ export async function adjustMargin(client, address, args) {
349
349
  if (args.poolOracleType !== undefined) {
350
350
  params.poolOracleType = Number(args.poolOracleType);
351
351
  }
352
- return client.position.adjustCollateral(params);
352
+ const result = await client.position.adjustCollateral(params);
353
+ if (result && typeof result === "object") {
354
+ result.__normalized = {
355
+ adjustAmountRaw: adjustAmount,
356
+ quoteToken,
357
+ };
358
+ }
359
+ return result;
353
360
  }
354
361
  /**
355
362
  * 平掉所有仓位
@@ -409,14 +416,14 @@ export async function updateOrderTpSl(client, address, args) {
409
416
  const { baseDecimals } = await resolveDecimalsForUpdateOrder(client, chainId, marketId, args.poolId);
410
417
  const params = {
411
418
  orderId: args.orderId,
412
- tpSize: ensureUnits(args.tpSize, baseDecimals, "tpSize"),
413
- tpPrice: ensureUnits(args.tpPrice, 30, "tpPrice"),
414
- slSize: ensureUnits(args.slSize, baseDecimals, "slSize"),
415
- slPrice: ensureUnits(args.slPrice, 30, "slPrice"),
419
+ tpSize: ensureUnits(args.tpSize, baseDecimals, "tpSize", { allowImplicitRaw: false }),
420
+ tpPrice: ensureUnits(args.tpPrice, 30, "tpPrice", { allowImplicitRaw: false }),
421
+ slSize: ensureUnits(args.slSize, baseDecimals, "slSize", { allowImplicitRaw: false }),
422
+ slPrice: ensureUnits(args.slPrice, 30, "slPrice", { allowImplicitRaw: false }),
416
423
  useOrderCollateral: Boolean(args.useOrderCollateral),
417
424
  executionFeeToken: quoteToken,
418
- size: ensureUnits(args.size, baseDecimals, "size"),
419
- price: ensureUnits(args.price, 30, "price"),
425
+ size: ensureUnits(args.size, baseDecimals, "size", { allowImplicitRaw: false }),
426
+ price: ensureUnits(args.price, 30, "price", { allowImplicitRaw: false }),
420
427
  };
421
428
  try {
422
429
  const result = await client.order.updateOrderTpSl(params, quoteToken, chainId, address, marketId, isTpSlOrder);
@@ -18,7 +18,7 @@ export const accountDepositTool = {
18
18
  // ensureUnits handles 'raw:' prefix if absolute precision is needed.
19
19
  const { ensureUnits } = await import("../utils/units.js");
20
20
  const { getQuoteDecimals } = await import("../auth/resolveClient.js");
21
- const amount = ensureUnits(args.amount, getQuoteDecimals(), "amount");
21
+ const amount = ensureUnits(args.amount, getQuoteDecimals(), "amount", { allowImplicitRaw: false });
22
22
  const raw = await client.account.deposit({
23
23
  amount,
24
24
  tokenAddress,
@@ -48,7 +48,7 @@ export const accountWithdrawTool = {
48
48
  const { getQuoteDecimals } = await import("../auth/resolveClient.js");
49
49
  // Assuming 18 decimals for base and quoteDecimals for quote
50
50
  const decimals = args.isQuoteToken ? getQuoteDecimals() : 18;
51
- const amount = ensureUnits(args.amount, decimals, "amount");
51
+ const amount = ensureUnits(args.amount, decimals, "amount", { allowImplicitRaw: false });
52
52
  const raw = await client.account.withdraw({
53
53
  chainId,
54
54
  receiver: address,
@@ -2,6 +2,7 @@ import { z } from "zod";
2
2
  import { resolveClient } from "../auth/resolveClient.js";
3
3
  import { adjustMargin as adjustMarginSvc } from "../services/tradeService.js";
4
4
  import { finalizeMutationResult } from "../utils/mutationResult.js";
5
+ import { extractErrorMessage } from "../utils/errorMessage.js";
5
6
  export const adjustMarginTool = {
6
7
  name: "adjust_margin",
7
8
  description: "Adjust the margin (collateral) of an open position.",
@@ -16,11 +17,16 @@ export const adjustMarginTool = {
16
17
  try {
17
18
  const { client, address, signer } = await resolveClient();
18
19
  const raw = await adjustMarginSvc(client, address, args);
20
+ const normalized = raw?.__normalized;
21
+ if (raw && typeof raw === "object" && "__normalized" in raw) {
22
+ delete raw.__normalized;
23
+ }
19
24
  const data = await finalizeMutationResult(raw, signer, "adjust_margin");
20
- return { content: [{ type: "text", text: JSON.stringify({ status: "success", data }, (_, v) => typeof v === 'bigint' ? v.toString() : v, 2) }] };
25
+ const payload = normalized ? { ...data, normalized } : data;
26
+ return { content: [{ type: "text", text: JSON.stringify({ status: "success", data: payload }, (_, v) => typeof v === 'bigint' ? v.toString() : v, 2) }] };
21
27
  }
22
28
  catch (error) {
23
- return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
29
+ return { content: [{ type: "text", text: `Error: ${extractErrorMessage(error)}` }], isError: true };
24
30
  }
25
31
  },
26
32
  };
@@ -4,7 +4,7 @@ import { resolveClient } from "../auth/resolveClient.js";
4
4
  import { finalizeMutationResult } from "../utils/mutationResult.js";
5
5
  export const createPerpMarketTool = {
6
6
  name: "create_perp_market",
7
- description: "Create a new perpetual contract pool on MYX. IMPORTANT: marketId cannot be randomly generated. It must be a valid 66-character config hash (0x...) tied to a supported quote token (like USDC). Use get_market_list to fetch an existing marketId if you don't have a specific newly allocated one.",
7
+ description: "Create a new perpetual contract pool on MYX. IMPORTANT: marketId cannot be randomly generated. It must be a valid 66-character config hash (0x...) tied to a supported quote token (like USDC). Use get_market_detail (after search_market/get_pool_list) to fetch an existing marketId if you don't have a specific newly allocated one.",
8
8
  schema: {
9
9
  baseToken: z.string().describe("Base token contract address (e.g., 0xb40aaadc43...)"),
10
10
  marketId: z.string().describe("MUST be a valid 66-char config hash (e.g., existing USDC marketId: 0x7f6727d8026fd2c87ccc745846c83cd0b68e886c73e1e05a54a675bcadd8adb6). Do NOT generate randomly."),
@@ -1,11 +1,14 @@
1
1
  import { z } from "zod";
2
- import { resolveClient } from "../auth/resolveClient.js";
2
+ import { resolveClient, getChainId } from "../auth/resolveClient.js";
3
3
  import { openPosition } from "../services/tradeService.js";
4
4
  import { finalizeMutationResult } from "../utils/mutationResult.js";
5
- import { SLIPPAGE_PCT_4DP_DESC } from "../utils/slippage.js";
5
+ import { normalizeSlippagePct4dp, SLIPPAGE_PCT_4DP_DESC } from "../utils/slippage.js";
6
6
  import { verifyTradeOutcome } from "../utils/verification.js";
7
7
  import { mapDirection, mapOrderType, mapTriggerType } from "../utils/mappings.js";
8
+ import { extractErrorMessage } from "../utils/errorMessage.js";
9
+ import { parseUserUnits } from "../utils/units.js";
8
10
  const POSITION_ID_RE = /^$|^0x[0-9a-fA-F]{64}$/;
11
+ const ZERO_POSITION_ID_RE = /^0x0{64}$/i;
9
12
  export const executeTradeTool = {
10
13
  name: "execute_trade",
11
14
  description: "Create an increase order using SDK-native parameters.",
@@ -13,7 +16,7 @@ export const executeTradeTool = {
13
16
  poolId: z.string().describe("Hex Pool ID, e.g. '0x14a19...'. Get via get_pool_list."),
14
17
  positionId: z.string().refine((value) => POSITION_ID_RE.test(value), {
15
18
  message: "positionId must be empty string for new position, or a bytes32 hex string.",
16
- }).describe("Position ID: Use empty string '' for NEW positions, or valid hex for INCREASING existing ones."),
19
+ }).describe("Position ID: Use empty string '' for NEW positions, or valid hex for INCREASING existing ones. 0x000..00 is auto-treated as NEW."),
17
20
  orderType: z.union([z.number(), z.string()]).describe("Market/Limit/Stop. e.g. 0 or 'MARKET'."),
18
21
  triggerType: z.union([z.number(), z.string()]).optional().describe("0=None (Market), 1=GTE, 2=LTE. e.g. 'GTE'."),
19
22
  direction: z.union([z.number(), z.string()]).describe("0/LONG/BUY or 1/SHORT/SELL."),
@@ -24,33 +27,105 @@ export const executeTradeTool = {
24
27
  postOnly: z.coerce.boolean().describe("If true, order only executes as Maker."),
25
28
  slippagePct: z.coerce.string().default("50").describe(`${SLIPPAGE_PCT_4DP_DESC}. Default is 50 (0.5%).`),
26
29
  executionFeeToken: z.string().describe("Address of token to pay gas/execution fees (typically USDC)."),
27
- leverage: z.coerce.number().describe("Leverage multiplier, e.g., 10 for 10x."),
30
+ leverage: z.coerce.number().positive().describe("Leverage multiplier, e.g., 10 for 10x."),
28
31
  tpSize: z.union([z.string(), z.number()]).optional().describe("Take Profit size. Use '0' to disable."),
29
32
  tpPrice: z.union([z.string(), z.number()]).optional().describe("Take Profit trigger price."),
30
33
  slSize: z.union([z.string(), z.number()]).optional().describe("Stop Loss size. Use '0' to disable."),
31
34
  slPrice: z.union([z.string(), z.number()]).optional().describe("Stop Loss trigger price."),
32
- tradingFee: z.union([z.string(), z.number()]).describe("Estimated fee in raw units. Fetch via get_user_trading_fee_rate."),
33
- marketId: z.string().describe("Specific Market Config Hash. Fetch via get_market_list."),
35
+ tradingFee: z.union([z.string(), z.number()]).optional().describe("Trading fee in quote token units. Supports human/raw prefix. Optional: auto-computed via get_user_trading_fee_rate."),
36
+ assetClass: z.coerce.number().int().nonnegative().optional().describe("Optional fee lookup assetClass (default from pool config or 1)."),
37
+ riskTier: z.coerce.number().int().nonnegative().optional().describe("Optional fee lookup riskTier (default from pool config or 1)."),
38
+ marketId: z.string().describe("Specific Market Config Hash. Fetch via get_market_detail (resolve poolId first with search_market/get_pool_list)."),
34
39
  },
35
40
  handler: async (args) => {
36
41
  try {
37
42
  const { client, address, signer } = await resolveClient();
43
+ const chainId = getChainId();
44
+ const normalizedPositionId = args.positionId === "0" || !args.positionId || ZERO_POSITION_ID_RE.test(String(args.positionId))
45
+ ? ""
46
+ : String(args.positionId);
38
47
  const poolId = args.poolId;
39
48
  // Fetch pool detail to get quoteToken for execution fee
40
- const poolResponse = await client.markets.getMarketDetail({ chainId: await resolveClient().then(r => r.chainId), poolId });
49
+ const poolResponse = await client.markets.getMarketDetail({ chainId, poolId });
41
50
  const poolData = poolResponse?.data || (poolResponse?.marketId ? poolResponse : null);
42
51
  if (!poolData)
43
52
  throw new Error(`Could not find pool metadata for ID: ${poolId}`);
53
+ const baseDecimals = Number(poolData.baseDecimals ?? 18);
54
+ const quoteDecimals = Number(poolData.quoteDecimals ?? 6);
55
+ const mappedDirection = mapDirection(args.direction);
56
+ const mappedOrderType = mapOrderType(args.orderType);
57
+ const mappedTriggerType = args.triggerType !== undefined ? mapTriggerType(args.triggerType) : undefined;
58
+ const slippagePctNormalized = normalizeSlippagePct4dp(args.slippagePct);
59
+ const collateralRaw = parseUserUnits(args.collateralAmount, quoteDecimals, "collateralAmount");
60
+ const sizeRaw = parseUserUnits(args.size, baseDecimals, "size");
61
+ const priceRaw = parseUserUnits(args.price, 30, "price");
62
+ if (BigInt(collateralRaw) <= 0n)
63
+ throw new Error("collateralAmount must be > 0.");
64
+ if (BigInt(sizeRaw) <= 0n)
65
+ throw new Error("size must be > 0.");
66
+ if (BigInt(priceRaw) <= 0n)
67
+ throw new Error("price must be > 0.");
68
+ let tradingFeeRaw = "";
69
+ let tradingFeeMeta = { source: "user" };
70
+ const tradingFeeInput = String(args.tradingFee ?? "").trim();
71
+ if (tradingFeeInput) {
72
+ tradingFeeRaw = parseUserUnits(tradingFeeInput, quoteDecimals, "tradingFee");
73
+ if (BigInt(tradingFeeRaw) < 0n)
74
+ throw new Error("tradingFee must be >= 0.");
75
+ }
76
+ else {
77
+ let poolAssetClass = 1;
78
+ let poolRiskTier = 1;
79
+ try {
80
+ const levelRes = await client.markets.getPoolLevelConfig(poolId, chainId);
81
+ const levelConfig = levelRes?.levelConfig || levelRes?.data?.levelConfig || {};
82
+ if (Number.isFinite(Number(levelConfig.assetClass))) {
83
+ poolAssetClass = Number(levelConfig.assetClass);
84
+ }
85
+ if (Number.isFinite(Number(levelConfig.riskTier))) {
86
+ poolRiskTier = Number(levelConfig.riskTier);
87
+ }
88
+ }
89
+ catch {
90
+ }
91
+ const assetClass = Number(args.assetClass ?? poolAssetClass ?? 1);
92
+ const riskTier = Number(args.riskTier ?? poolRiskTier ?? 1);
93
+ const feeRes = await client.utils.getUserTradingFeeRate(assetClass, riskTier, chainId);
94
+ if (Number(feeRes?.code) !== 0 || !feeRes?.data) {
95
+ throw new Error(`Failed to compute tradingFee automatically (assetClass=${assetClass}, riskTier=${riskTier}). Provide tradingFee manually.`);
96
+ }
97
+ const rateRaw = args.postOnly ? feeRes.data.makerFeeRate : feeRes.data.takerFeeRate;
98
+ const rateBig = BigInt(String(rateRaw ?? "0"));
99
+ tradingFeeRaw = ((BigInt(collateralRaw) * rateBig) / 1000000n).toString();
100
+ tradingFeeMeta = { source: "computed", assetClass, riskTier, feeRate: String(rateRaw ?? "0") };
101
+ }
44
102
  const mappedArgs = {
45
103
  ...args,
46
- direction: mapDirection(args.direction),
47
- orderType: mapOrderType(args.orderType),
48
- triggerType: args.triggerType !== undefined ? mapTriggerType(args.triggerType) : undefined,
104
+ direction: mappedDirection,
105
+ orderType: mappedOrderType,
106
+ triggerType: mappedTriggerType,
49
107
  // Normalize positionId
50
- positionId: (args.positionId === "0" || !args.positionId) ? "" : args.positionId,
108
+ positionId: normalizedPositionId,
51
109
  // Enforce executionFeeToken as quoteToken
52
- executionFeeToken: poolData.quoteToken || args.executionFeeToken
110
+ executionFeeToken: poolData.quoteToken || args.executionFeeToken,
111
+ collateralAmount: `raw:${collateralRaw}`,
112
+ size: `raw:${sizeRaw}`,
113
+ price: `raw:${priceRaw}`,
114
+ tradingFee: `raw:${tradingFeeRaw}`,
115
+ slippagePct: slippagePctNormalized,
53
116
  };
117
+ if (args.tpSize !== undefined) {
118
+ mappedArgs.tpSize = `raw:${parseUserUnits(args.tpSize, baseDecimals, "tpSize")}`;
119
+ }
120
+ if (args.tpPrice !== undefined) {
121
+ mappedArgs.tpPrice = `raw:${parseUserUnits(args.tpPrice, 30, "tpPrice")}`;
122
+ }
123
+ if (args.slSize !== undefined) {
124
+ mappedArgs.slSize = `raw:${parseUserUnits(args.slSize, baseDecimals, "slSize")}`;
125
+ }
126
+ if (args.slPrice !== undefined) {
127
+ mappedArgs.slPrice = `raw:${parseUserUnits(args.slPrice, 30, "slPrice")}`;
128
+ }
54
129
  const raw = await openPosition(client, address, mappedArgs);
55
130
  const data = await finalizeMutationResult(raw, signer, "execute_trade");
56
131
  const txHash = data.confirmation?.txHash;
@@ -58,11 +133,27 @@ export const executeTradeTool = {
58
133
  if (txHash) {
59
134
  verification = await verifyTradeOutcome(client, address, args.poolId, txHash);
60
135
  }
61
- const payload = { ...data, verification };
136
+ const payload = {
137
+ ...data,
138
+ verification,
139
+ preflight: {
140
+ normalized: {
141
+ collateralAmountRaw: collateralRaw,
142
+ sizeRaw,
143
+ priceRaw30: priceRaw,
144
+ tradingFeeRaw,
145
+ tpSizeRaw: mappedArgs.tpSize?.replace(/^raw:/i, "") ?? null,
146
+ tpPriceRaw30: mappedArgs.tpPrice?.replace(/^raw:/i, "") ?? null,
147
+ slSizeRaw: mappedArgs.slSize?.replace(/^raw:/i, "") ?? null,
148
+ slPriceRaw30: mappedArgs.slPrice?.replace(/^raw:/i, "") ?? null,
149
+ },
150
+ tradingFeeMeta,
151
+ },
152
+ };
62
153
  return { content: [{ type: "text", text: JSON.stringify({ status: "success", data: payload }, (k, v) => typeof v === 'bigint' ? v.toString() : v, 2) }] };
63
154
  }
64
155
  catch (error) {
65
- return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
156
+ return { content: [{ type: "text", text: `Error: ${extractErrorMessage(error)}` }], isError: true };
66
157
  }
67
158
  },
68
159
  };
@@ -0,0 +1,265 @@
1
+ import { z } from "zod";
2
+ import { resolveClient, getChainId } from "../auth/resolveClient.js";
3
+ import { COMMON_LP_AMOUNT_DECIMALS } from "@myx-trade/sdk";
4
+ import { Contract, formatUnits } from "ethers";
5
+ import { extractErrorMessage } from "../utils/errorMessage.js";
6
+ const ADDRESS_RE = /^0x[a-fA-F0-9]{40}$/;
7
+ const ERC20_BALANCE_ABI = ["function balanceOf(address owner) view returns (uint256)"];
8
+ function collectRows(input) {
9
+ if (Array.isArray(input))
10
+ return input.flatMap(collectRows);
11
+ if (!input || typeof input !== "object")
12
+ return [];
13
+ if (input.poolId || input.pool_id)
14
+ return [input];
15
+ return Object.values(input).flatMap(collectRows);
16
+ }
17
+ function readAddress(value) {
18
+ const text = String(value ?? "").trim();
19
+ if (!text || !ADDRESS_RE.test(text))
20
+ return null;
21
+ return text;
22
+ }
23
+ function normalizePoolId(value) {
24
+ return String(value ?? "").trim();
25
+ }
26
+ function normalizeAssetSymbol(value) {
27
+ const text = String(value ?? "").trim();
28
+ if (!text)
29
+ return "";
30
+ return text.replace(/\s+/g, "").replace(/\//g, "").toUpperCase();
31
+ }
32
+ function parsePairSymbols(value) {
33
+ const text = String(value ?? "").trim();
34
+ if (!text)
35
+ return null;
36
+ const parts = text
37
+ .split(/[\/:_-]/)
38
+ .map((item) => normalizeAssetSymbol(item))
39
+ .filter(Boolean);
40
+ if (parts.length >= 2) {
41
+ return { base: parts[0], quote: parts[1] };
42
+ }
43
+ return null;
44
+ }
45
+ function resolveBaseQuoteSymbols(row, detail) {
46
+ const baseCandidates = [
47
+ row?.baseSymbol,
48
+ detail?.baseSymbol,
49
+ row?.base_symbol,
50
+ detail?.base_symbol,
51
+ ];
52
+ const quoteCandidates = [
53
+ row?.quoteSymbol,
54
+ detail?.quoteSymbol,
55
+ row?.quote_symbol,
56
+ detail?.quote_symbol,
57
+ ];
58
+ let baseSymbol = baseCandidates.map((item) => normalizeAssetSymbol(item)).find(Boolean) || "";
59
+ let quoteSymbol = quoteCandidates.map((item) => normalizeAssetSymbol(item)).find(Boolean) || "";
60
+ if (!baseSymbol || !quoteSymbol) {
61
+ const pairCandidate = row?.baseQuoteSymbol ??
62
+ detail?.baseQuoteSymbol ??
63
+ row?.symbol ??
64
+ detail?.symbol ??
65
+ row?.symbolName ??
66
+ detail?.symbolName;
67
+ const parsed = parsePairSymbols(pairCandidate);
68
+ if (parsed) {
69
+ baseSymbol = baseSymbol || parsed.base;
70
+ quoteSymbol = quoteSymbol || parsed.quote;
71
+ }
72
+ }
73
+ return {
74
+ baseSymbol: baseSymbol || null,
75
+ quoteSymbol: quoteSymbol || null,
76
+ };
77
+ }
78
+ function buildLpAssetNames(baseSymbol, quoteSymbol) {
79
+ if (!baseSymbol || !quoteSymbol) {
80
+ return { baseLpAssetName: null, quoteLpAssetName: null };
81
+ }
82
+ return {
83
+ baseLpAssetName: `m${baseSymbol}.${quoteSymbol}`,
84
+ quoteLpAssetName: `m${quoteSymbol}.${baseSymbol}`,
85
+ };
86
+ }
87
+ function normalizePoolIdsInput(input) {
88
+ if (Array.isArray(input)) {
89
+ return input.map((item) => normalizePoolId(item)).filter(Boolean);
90
+ }
91
+ if (typeof input !== "string")
92
+ return [];
93
+ const text = input.trim();
94
+ if (!text)
95
+ return [];
96
+ if (text.startsWith("[") && text.endsWith("]")) {
97
+ try {
98
+ const parsed = JSON.parse(text);
99
+ if (Array.isArray(parsed)) {
100
+ return parsed.map((item) => normalizePoolId(item)).filter(Boolean);
101
+ }
102
+ }
103
+ catch {
104
+ }
105
+ }
106
+ if (text.includes(",")) {
107
+ return text.split(",").map((item) => normalizePoolId(item)).filter(Boolean);
108
+ }
109
+ return [text];
110
+ }
111
+ function toSymbol(row) {
112
+ const baseQuote = String(row?.baseQuoteSymbol ?? row?.symbol ?? "").trim();
113
+ if (baseQuote)
114
+ return baseQuote;
115
+ const base = String(row?.baseSymbol ?? "").trim();
116
+ const quote = String(row?.quoteSymbol ?? "").trim();
117
+ if (base && quote)
118
+ return `${base}/${quote}`;
119
+ if (base)
120
+ return base;
121
+ return normalizePoolId(row?.poolId ?? row?.pool_id);
122
+ }
123
+ function sumRaw(baseRaw, quoteRaw) {
124
+ return BigInt(baseRaw || "0") + BigInt(quoteRaw || "0");
125
+ }
126
+ async function readErc20Balance(provider, tokenAddress, holder) {
127
+ const contract = new Contract(tokenAddress, ERC20_BALANCE_ABI, provider);
128
+ const balance = await contract.balanceOf(holder);
129
+ return BigInt(balance).toString();
130
+ }
131
+ export const getMyLpHoldingsTool = {
132
+ name: "get_my_lp_holdings",
133
+ description: "List your LP holdings across pools on the current chain by reading base/quote LP token balances. Includes standardized LP asset names: base LP `mBASE.QUOTE`, quote LP `mQUOTE.BASE`.",
134
+ schema: {
135
+ includeZero: z.coerce.boolean().optional().describe("If true, include pools with zero LP balances (default false)."),
136
+ poolIds: z.union([z.array(z.string()).min(1), z.string().min(1)]).optional().describe("Optional poolId filter. Supports array, JSON-array string, comma string, or single poolId."),
137
+ maxPools: z.coerce.number().int().positive().max(2000).optional().describe("Optional cap for scanned pools (default all)."),
138
+ },
139
+ handler: async (args) => {
140
+ try {
141
+ const { client, address, signer } = await resolveClient();
142
+ const chainId = getChainId();
143
+ const provider = signer?.provider;
144
+ if (!provider) {
145
+ throw new Error("Provider is unavailable for LP balance reads.");
146
+ }
147
+ const includeZero = !!args.includeZero;
148
+ const poolIdsFilter = normalizePoolIdsInput(args.poolIds);
149
+ const filterSet = new Set(poolIdsFilter.map((item) => item.toLowerCase()));
150
+ const maxPools = Number.isFinite(Number(args.maxPools)) ? Math.floor(Number(args.maxPools)) : null;
151
+ const poolListRes = await client.api.getPoolList();
152
+ const rows = collectRows(poolListRes?.data ?? poolListRes);
153
+ const deduped = new Map();
154
+ for (const row of rows) {
155
+ const poolId = normalizePoolId(row?.poolId ?? row?.pool_id);
156
+ if (!poolId)
157
+ continue;
158
+ if (filterSet.size > 0 && !filterSet.has(poolId.toLowerCase()))
159
+ continue;
160
+ if (!deduped.has(poolId.toLowerCase())) {
161
+ deduped.set(poolId.toLowerCase(), row);
162
+ }
163
+ }
164
+ const selectedRows = Array.from(deduped.values());
165
+ const scannedRows = maxPools ? selectedRows.slice(0, maxPools) : selectedRows;
166
+ const items = [];
167
+ const warnings = [];
168
+ let totalBaseLpRaw = 0n;
169
+ let totalQuoteLpRaw = 0n;
170
+ for (const row of scannedRows) {
171
+ const poolId = normalizePoolId(row?.poolId ?? row?.pool_id);
172
+ let basePoolToken = readAddress(row?.basePoolToken ?? row?.base_pool_token);
173
+ let quotePoolToken = readAddress(row?.quotePoolToken ?? row?.quote_pool_token);
174
+ let detail = null;
175
+ if (!basePoolToken || !quotePoolToken) {
176
+ try {
177
+ const detailRes = await client.markets.getMarketDetail({ chainId, poolId });
178
+ detail = detailRes?.data || (detailRes?.marketId ? detailRes : null);
179
+ basePoolToken = basePoolToken ?? readAddress(detail?.basePoolToken ?? detail?.base_pool_token);
180
+ quotePoolToken = quotePoolToken ?? readAddress(detail?.quotePoolToken ?? detail?.quote_pool_token);
181
+ }
182
+ catch (error) {
183
+ warnings.push(`pool ${poolId}: failed to enrich pool detail (${extractErrorMessage(error)})`);
184
+ }
185
+ }
186
+ const { baseSymbol, quoteSymbol } = resolveBaseQuoteSymbols(row, detail);
187
+ const { baseLpAssetName, quoteLpAssetName } = buildLpAssetNames(baseSymbol, quoteSymbol);
188
+ let baseLpRaw = "0";
189
+ let quoteLpRaw = "0";
190
+ if (basePoolToken) {
191
+ try {
192
+ baseLpRaw = await readErc20Balance(provider, basePoolToken, address);
193
+ }
194
+ catch (error) {
195
+ warnings.push(`pool ${poolId}: failed to read base LP balance (${extractErrorMessage(error)})`);
196
+ }
197
+ }
198
+ if (quotePoolToken) {
199
+ try {
200
+ quoteLpRaw = await readErc20Balance(provider, quotePoolToken, address);
201
+ }
202
+ catch (error) {
203
+ warnings.push(`pool ${poolId}: failed to read quote LP balance (${extractErrorMessage(error)})`);
204
+ }
205
+ }
206
+ const hasAnyLp = BigInt(baseLpRaw) > 0n || BigInt(quoteLpRaw) > 0n;
207
+ if (!includeZero && !hasAnyLp)
208
+ continue;
209
+ totalBaseLpRaw += BigInt(baseLpRaw);
210
+ totalQuoteLpRaw += BigInt(quoteLpRaw);
211
+ items.push({
212
+ poolId,
213
+ symbol: toSymbol(row),
214
+ state: row?.state ?? row?.poolState ?? null,
215
+ baseSymbol,
216
+ quoteSymbol,
217
+ baseLpAssetName,
218
+ quoteLpAssetName,
219
+ basePoolToken: basePoolToken ?? null,
220
+ quotePoolToken: quotePoolToken ?? null,
221
+ baseLpBalanceRaw: baseLpRaw,
222
+ baseLpBalance: formatUnits(baseLpRaw, COMMON_LP_AMOUNT_DECIMALS),
223
+ quoteLpBalanceRaw: quoteLpRaw,
224
+ quoteLpBalance: formatUnits(quoteLpRaw, COMMON_LP_AMOUNT_DECIMALS),
225
+ hasAnyLp,
226
+ });
227
+ }
228
+ items.sort((left, right) => {
229
+ const leftSum = sumRaw(left.baseLpBalanceRaw, left.quoteLpBalanceRaw);
230
+ const rightSum = sumRaw(right.baseLpBalanceRaw, right.quoteLpBalanceRaw);
231
+ if (leftSum === rightSum)
232
+ return 0;
233
+ return rightSum > leftSum ? 1 : -1;
234
+ });
235
+ const payload = {
236
+ meta: {
237
+ address,
238
+ chainId,
239
+ includeZero,
240
+ requestedPoolIds: poolIdsFilter,
241
+ scannedPools: scannedRows.length,
242
+ totalDiscoveredPools: selectedRows.length,
243
+ maxPools: maxPools ?? null,
244
+ },
245
+ summary: {
246
+ heldPools: items.length,
247
+ totalBaseLpRaw: totalBaseLpRaw.toString(),
248
+ totalBaseLp: formatUnits(totalBaseLpRaw, COMMON_LP_AMOUNT_DECIMALS),
249
+ totalQuoteLpRaw: totalQuoteLpRaw.toString(),
250
+ totalQuoteLp: formatUnits(totalQuoteLpRaw, COMMON_LP_AMOUNT_DECIMALS),
251
+ },
252
+ items,
253
+ };
254
+ if (warnings.length > 0) {
255
+ payload.warnings = warnings.slice(0, 100);
256
+ }
257
+ return {
258
+ content: [{ type: "text", text: JSON.stringify({ status: "success", data: payload }, (_, value) => typeof value === "bigint" ? value.toString() : value, 2) }],
259
+ };
260
+ }
261
+ catch (error) {
262
+ return { content: [{ type: "text", text: `Error: ${extractErrorMessage(error)}` }], isError: true };
263
+ }
264
+ },
265
+ };
@@ -31,7 +31,6 @@ export { getPositionsTool } from "./getPositions.js";
31
31
  export { getOpenOrdersTool, getOrderHistoryTool } from "./orderQueries.js";
32
32
  export { getPositionHistoryTool } from "./positionHistory.js";
33
33
  export { getAccountTool, getTradeFlowTool } from "./accountInfo.js";
34
+ export { getMyLpHoldingsTool } from "./getMyLpHoldings.js";
34
35
  export { getAccountVipInfoTool } from "./getAccountVipInfo.js";
35
36
  export { accountDepositTool, accountWithdrawTool } from "./accountTransfer.js";
36
- export { getMarketListTool } from "./getMarketList.js";
37
- export { getMarketListRawTool } from "./getMarketListRaw.js";
@@ -1,12 +1,53 @@
1
1
  import { z } from "zod";
2
2
  import { quoteDeposit, quoteWithdraw, baseDeposit, baseWithdraw, getLpPrice, } from "../services/poolService.js";
3
- import { resolveClient } from "../auth/resolveClient.js";
3
+ import { resolveClient, getChainId } from "../auth/resolveClient.js";
4
4
  import { resolvePool } from "../services/marketService.js";
5
5
  import { finalizeMutationResult } from "../utils/mutationResult.js";
6
6
  import { extractErrorMessage } from "../utils/errorMessage.js";
7
+ function normalizeAssetSymbol(value) {
8
+ const text = String(value ?? "").trim();
9
+ if (!text)
10
+ return "";
11
+ return text.replace(/\s+/g, "").replace(/\//g, "").toUpperCase();
12
+ }
13
+ function parsePairSymbols(value) {
14
+ const text = String(value ?? "").trim();
15
+ if (!text)
16
+ return null;
17
+ const parts = text
18
+ .split(/[\/:_-]/)
19
+ .map((item) => normalizeAssetSymbol(item))
20
+ .filter(Boolean);
21
+ if (parts.length >= 2) {
22
+ return { base: parts[0], quote: parts[1] };
23
+ }
24
+ return null;
25
+ }
26
+ function resolveLpAssetNames(detail) {
27
+ let baseSymbol = normalizeAssetSymbol(detail?.baseSymbol ?? detail?.base_symbol);
28
+ let quoteSymbol = normalizeAssetSymbol(detail?.quoteSymbol ?? detail?.quote_symbol);
29
+ if (!baseSymbol || !quoteSymbol) {
30
+ const pairCandidate = detail?.baseQuoteSymbol ?? detail?.symbol ?? detail?.symbolName;
31
+ const parsed = parsePairSymbols(pairCandidate);
32
+ if (parsed) {
33
+ baseSymbol = baseSymbol || parsed.base;
34
+ quoteSymbol = quoteSymbol || parsed.quote;
35
+ }
36
+ }
37
+ const normalizedBase = baseSymbol || null;
38
+ const normalizedQuote = quoteSymbol || null;
39
+ const baseLpAssetName = normalizedBase && normalizedQuote ? `m${normalizedBase}.${normalizedQuote}` : null;
40
+ const quoteLpAssetName = normalizedBase && normalizedQuote ? `m${normalizedQuote}.${normalizedBase}` : null;
41
+ return {
42
+ baseSymbol: normalizedBase,
43
+ quoteSymbol: normalizedQuote,
44
+ baseLpAssetName,
45
+ quoteLpAssetName,
46
+ };
47
+ }
7
48
  export const manageLiquidityTool = {
8
49
  name: "manage_liquidity",
9
- description: "Add or withdraw liquidity from a BASE or QUOTE pool.",
50
+ description: "Add or withdraw liquidity from a BASE or QUOTE pool. Success response includes LP naming metadata: base `mBASE.QUOTE`, quote `mQUOTE.BASE`, plus `operatedLpAssetName` based on poolType.",
10
51
  schema: {
11
52
  action: z.enum(["deposit", "withdraw", "add", "remove", "increase", "decrease"]).describe("'deposit' or 'withdraw' (aliases: add, remove, increase, decrease)"),
12
53
  poolType: z.enum(["BASE", "QUOTE"]).describe("'BASE' or 'QUOTE'"),
@@ -20,23 +61,31 @@ export const manageLiquidityTool = {
20
61
  const { client, signer } = await resolveClient();
21
62
  let { action, poolType, poolId } = args;
22
63
  const { amount, slippage } = args;
64
+ const chainId = args.chainId ?? getChainId();
23
65
  // 1. Action Alias Mapping
24
66
  if (action === "add" || action === "increase")
25
67
  action = "deposit";
26
68
  if (action === "remove" || action === "decrease")
27
69
  action = "withdraw";
28
70
  // 2. Smart Pool Resolution (Handles PoolId, Token Address, or Keywords)
29
- poolId = await resolvePool(client, poolId);
71
+ poolId = await resolvePool(client, poolId, undefined, chainId);
72
+ // 3. Preflight pool validation for target chain (avoid opaque SDK "Invalid Params")
73
+ const detailRes = await client.markets.getMarketDetail({ chainId, poolId }).catch(() => null);
74
+ const detail = detailRes?.data || (detailRes?.marketId ? detailRes : null);
75
+ if (!detail?.marketId) {
76
+ throw new Error(`Pool ${poolId} not found on chainId ${chainId}. ` +
77
+ `Please query a valid active pool via search_market/get_pool_list first.`);
78
+ }
30
79
  let raw;
31
80
  if (poolType === "QUOTE") {
32
81
  raw = action === "deposit"
33
- ? await quoteDeposit(poolId, amount, slippage, args.chainId)
34
- : await quoteWithdraw(poolId, amount, slippage, args.chainId);
82
+ ? await quoteDeposit(poolId, amount, slippage, chainId)
83
+ : await quoteWithdraw(poolId, amount, slippage, chainId);
35
84
  }
36
85
  else {
37
86
  raw = action === "deposit"
38
- ? await baseDeposit(poolId, amount, slippage, args.chainId)
39
- : await baseWithdraw(poolId, amount, slippage, args.chainId);
87
+ ? await baseDeposit(poolId, amount, slippage, chainId)
88
+ : await baseWithdraw(poolId, amount, slippage, chainId);
40
89
  }
41
90
  if (!raw) {
42
91
  throw new Error(`SDK returned an empty result for liquidity ${action}. This usually occurs if the pool is not in an Active state (state: 2) or if there is a contract-level restriction. Please check pool_info.`);
@@ -45,7 +94,26 @@ export const manageLiquidityTool = {
45
94
  throw new Error(`Liquidity ${action} failed: ${extractErrorMessage(raw)}`);
46
95
  }
47
96
  const data = await finalizeMutationResult(raw, signer, "manage_liquidity");
48
- return { content: [{ type: "text", text: JSON.stringify({ status: "success", data }, (_, v) => typeof v === 'bigint' ? v.toString() : v, 2) }] };
97
+ const lpAssetNames = resolveLpAssetNames(detail);
98
+ const operatedLpAssetName = poolType === "BASE" ? lpAssetNames.baseLpAssetName : lpAssetNames.quoteLpAssetName;
99
+ const payload = data && typeof data === "object" && !Array.isArray(data)
100
+ ? {
101
+ ...data,
102
+ lpAssetNames: {
103
+ ...lpAssetNames,
104
+ operatedPoolType: poolType,
105
+ operatedLpAssetName,
106
+ },
107
+ }
108
+ : {
109
+ result: data,
110
+ lpAssetNames: {
111
+ ...lpAssetNames,
112
+ operatedPoolType: poolType,
113
+ operatedLpAssetName,
114
+ },
115
+ };
116
+ return { content: [{ type: "text", text: JSON.stringify({ status: "success", data: payload }, (_, v) => typeof v === 'bigint' ? v.toString() : v, 2) }] };
49
117
  }
50
118
  catch (error) {
51
119
  return { content: [{ type: "text", text: `Error: ${extractErrorMessage(error)}` }], isError: true };
@@ -1,6 +1,7 @@
1
1
  import { parseUnits } from "ethers";
2
2
  const DECIMAL_RE = /^-?\d+(\.\d+)?$/;
3
3
  const RAW_PREFIX_RE = /^raw:/i;
4
+ const HUMAN_PREFIX_RE = /^human:/i;
4
5
  const INTEGER_RE = /^-?\d+$/;
5
6
  function normalizeDecimal(input) {
6
7
  let value = input.trim();
@@ -19,10 +20,14 @@ function normalizeDecimal(input) {
19
20
  }
20
21
  return `${sign}${intPart || "0"}.${fracPart}`;
21
22
  }
22
- export function ensureUnits(value, decimals, label = "value") {
23
+ export function ensureUnits(value, decimals, label = "value", options = {}) {
24
+ const allowImplicitRaw = options.allowImplicitRaw ?? true;
23
25
  let str = String(value).trim();
24
26
  if (!str)
25
27
  throw new Error(`${label} is required.`);
28
+ if (HUMAN_PREFIX_RE.test(str)) {
29
+ str = str.replace(HUMAN_PREFIX_RE, "").trim();
30
+ }
26
31
  if (RAW_PREFIX_RE.test(str)) {
27
32
  const raw = str.replace(RAW_PREFIX_RE, "").trim();
28
33
  if (!INTEGER_RE.test(raw))
@@ -39,9 +44,8 @@ export function ensureUnits(value, decimals, label = "value") {
39
44
  str = `${parts[0]}.${parts[1].slice(0, decimals)}`;
40
45
  }
41
46
  }
42
- // If it's already a very large integer (e.g. > 12 digits or > decimals digits),
43
- // assume it's already in the smallest unit (Wei/Raw).
44
- if (!str.includes(".") && (str.length > 12 || str.length > decimals)) {
47
+ // Legacy compatibility: optionally treat very large integers as already raw.
48
+ if (allowImplicitRaw && !str.includes(".") && (str.length > 12 || str.length > decimals)) {
45
49
  return str;
46
50
  }
47
51
  try {
@@ -53,9 +57,12 @@ export function ensureUnits(value, decimals, label = "value") {
53
57
  }
54
58
  }
55
59
  export function parseUserUnits(value, decimals, label = "value") {
56
- const str = String(value).trim();
60
+ let str = String(value).trim();
57
61
  if (!str)
58
62
  throw new Error(`${label} is required.`);
63
+ if (HUMAN_PREFIX_RE.test(str)) {
64
+ str = str.replace(HUMAN_PREFIX_RE, "").trim();
65
+ }
59
66
  if (RAW_PREFIX_RE.test(str)) {
60
67
  const raw = str.replace(RAW_PREFIX_RE, "").trim();
61
68
  if (!INTEGER_RE.test(raw))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@michaleffffff/mcp-trading-server",
3
- "version": "3.0.1",
3
+ "version": "3.0.3",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "myx-mcp": "dist/server.js"
@@ -1,20 +0,0 @@
1
- import { resolveClient } from "../auth/resolveClient.js";
2
- import { searchMarket } from "../services/marketService.js";
3
- import { z } from "zod";
4
- export const getMarketListTool = {
5
- name: "get_market_list",
6
- description: "Get tradable markets/pools (state=2 Active). Supports configurable result limit; backend may still enforce its own cap.",
7
- schema: {
8
- limit: z.coerce.number().int().positive().optional().describe("Max results to request (default 1000)."),
9
- },
10
- handler: async (args) => {
11
- try {
12
- const { client } = await resolveClient();
13
- const activeMarkets = await searchMarket(client, "", args.limit ?? 1000);
14
- return { content: [{ type: "text", text: JSON.stringify({ status: "success", data: activeMarkets }, (_, v) => typeof v === 'bigint' ? v.toString() : v, 2) }] };
15
- }
16
- catch (error) {
17
- return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
18
- }
19
- },
20
- };
@@ -1,16 +0,0 @@
1
- import { resolveClient } from "../auth/resolveClient.js";
2
- export const getMarketListRawTool = {
3
- name: "get_market_list_raw",
4
- description: "Get raw market list directly from MYX API without MCP-side filtering.",
5
- schema: {},
6
- handler: async () => {
7
- try {
8
- const { client } = await resolveClient();
9
- const result = await client.api.getMarketList();
10
- return { content: [{ type: "text", text: JSON.stringify({ status: "success", data: result }, (_, v) => typeof v === "bigint" ? v.toString() : v, 2) }] };
11
- }
12
- catch (error) {
13
- return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
14
- }
15
- },
16
- };