@michaleffffff/mcp-trading-server 3.0.0 → 3.0.2

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,27 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.0.2 - 2026-03-17
4
+
5
+ ### Added
6
+ - Integrated `verifyTradeOutcome` in `execute_trade` for post-transaction state validation.
7
+ - Automatic `tradingFee` calculation in `execute_trade` using `getUserTradingFeeRate`.
8
+ - `parseUserUnits` / `parseUserPrice30` utilities for more consistent human/raw input handling.
9
+
10
+ ### Changed
11
+ - `execute_trade` now surfaces `preflight` normalization details in the successful response.
12
+ - `adjust_margin` now supports displaying `__normalized` data.
13
+ - Refined unit resolution in `account_deposit` and `account_withdraw`.
14
+
15
+ ## 3.0.1 - 2026-03-17
16
+
17
+ ### Added
18
+ - `extractErrorMessage` utility for cleaner error reporting across all tools.
19
+
20
+ ### Changed
21
+ - `manage_liquidity` now performs strict SDK code validation before finalizing transactions.
22
+ - `cancel_all_orders` improved to handle comma-separated and JSON-array strings for `orderIds`.
23
+ - Integrated meaningful error extraction into `marketInfo`, `manageLiquidity`, and `updateOrderTpSl`.
24
+
3
25
  ## 3.0.0 - 2026-03-17
4
26
 
5
27
  ### Breaking Changes
package/README.md CHANGED
@@ -161,6 +161,7 @@ The server will run using `stdio` transport and can be connected by any MCP-comp
161
161
  - **Price**: `"price"` is human by default and will be converted to **30-decimal** format internally. Use `raw:` to pass a 30-decimal integer directly.
162
162
  - **Slippage**: `slippagePct` uses 4-decimal raw units: `100` = `1%`, `50` = `0.5%`, `1000` = `10%`.
163
163
  - **Note**: Some account-transfer tools (e.g. `account_deposit`, `account_withdraw`, `check_approval`) use **raw integer strings**.
164
+ - **`adjust_margin` units**: human amount is supported (e.g. `"1"` means 1 quote token). For exact atomic amount, use `raw:` (e.g. `"raw:1"`).
164
165
 
165
166
  ## P0 Reliability Contract (AI-friendly)
166
167
 
@@ -290,12 +291,13 @@ For practical tool-by-tool examples, see: `TOOL_EXAMPLES.md`
290
291
 
291
292
  ### Trading Operations
292
293
  * **open_position_simple**: High-level open position helper (recommended). Computes price/size/tradingFee internally.
293
- * **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).
294
295
  * **close_position**: Close an open position.
295
296
  * **close_all_positions**: Emergency: close ALL open positions in a pool at once.
296
297
  * **cancel_order**: Cancel an open order by its order ID.
297
- * **cancel_all_orders**: Cancel multiple open orders by order IDs (use `get_open_orders` first to get IDs).
298
+ * **cancel_all_orders**: Cancel open orders by IDs (supports array, JSON-array string, comma string, or single ID; use `get_open_orders` first).
298
299
  * **set_tp_sl**: Set take profit and stop loss prices for an open position.
300
+ * **update_order_tp_sl**: Update TP/SL fields for an existing order. Accepts boolean-like values for `useOrderCollateral` / `isTpSlOrder`.
299
301
  * **adjust_margin**: Adjust the margin (collateral) of an open position.
300
302
  * **get_user_trading_fee_rate**: Query current maker/taker fee rates by asset class and risk tier.
301
303
  * **get_network_fee**: Query estimated network fee requirements for a market.
@@ -306,7 +308,7 @@ For practical tool-by-tool examples, see: `TOOL_EXAMPLES.md`
306
308
  * **get_oracle_price**: Get the current EVM oracle price for a specific pool.
307
309
  * **get_kline_latest_bar**: Get only the latest bar for an interval.
308
310
  * **get_all_tickers**: Get all ticker snapshots in one request.
309
- * **get_market_list**, **get_market_list_raw**, **get_kline**, **get_pool_info**...
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)
310
312
  * **get_pool_symbol_all**, **get_base_detail**, **get_pool_level_config**...
311
313
 
312
314
  ### Account & Portfolio
@@ -321,7 +323,7 @@ For practical tool-by-tool examples, see: `TOOL_EXAMPLES.md`
321
323
  - If one section fails (wallet or trading-account), the tool may return a **partial** snapshot with `meta.partial=true` and section-level `error` details.
322
324
 
323
325
  ### Liquidity Provision (LP)
324
- * **manage_liquidity**: Add or withdraw liquidity from a BASE or QUOTE pool.
326
+ * **manage_liquidity**: Add or withdraw liquidity from a BASE or QUOTE pool (aliases: `add/remove/increase/decrease` are supported).
325
327
  * **create_perp_market**: Create a new perpetual trading pair.
326
328
 
327
329
  ---
@@ -371,8 +373,15 @@ Use this query/mutation loop for limit-order management:
371
373
 
372
374
  1. Place order with `execute_trade` (`orderType=LIMIT`).
373
375
  2. Query active orders with `get_open_orders`.
374
- 3. Check historical fills/cancellations with `get_order_history`.
375
- 4. Cancel one order via `cancel_order` or batch via `cancel_all_orders`.
376
+ 3. If still pending, you can cancel/update order-level fields; if filled into a position, prefer `set_tp_sl` for position TP/SL.
377
+ 4. Check historical fills/cancellations with `get_order_history`.
378
+ 5. Cancel one order via `cancel_order` or batch via `cancel_all_orders`.
379
+
380
+ ## Unit Safety (P0)
381
+
382
+ - Monetary fields in major mutation tools support `human:` and `raw:` prefixes.
383
+ - Default behavior is human-readable units (e.g. `1` means `1.0` token), while `raw:<int>` enforces exact raw units.
384
+ - `execute_trade` and `adjust_margin` responses include normalized raw values for auditability.
376
385
 
377
386
  Minimal query examples:
378
387
 
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
@@ -140,6 +143,15 @@ It includes:
140
143
  }
141
144
  }
142
145
  ```
146
+ Single-ID string is also accepted:
147
+ ```json
148
+ {
149
+ "name": "cancel_all_orders",
150
+ "arguments": {
151
+ "orderIds": "0xORDER_ID_1"
152
+ }
153
+ }
154
+ ```
143
155
 
144
156
  ### `set_tp_sl`
145
157
  ```json
@@ -180,6 +192,28 @@ It includes:
180
192
  }
181
193
  }
182
194
  ```
195
+ String booleans are also accepted:
196
+ ```json
197
+ {
198
+ "name": "update_order_tp_sl",
199
+ "arguments": {
200
+ "orderId": "0xORDER_ID",
201
+ "marketId": "0xMARKET_ID",
202
+ "size": 0.01,
203
+ "price": 65000,
204
+ "tpPrice": 70000,
205
+ "tpSize": 0.005,
206
+ "slPrice": 60000,
207
+ "slSize": 0.005,
208
+ "useOrderCollateral": "true",
209
+ "isTpSlOrder": "true",
210
+ "quoteToken": "0xQUOTE_TOKEN"
211
+ }
212
+ }
213
+ ```
214
+ Note:
215
+ - This tool is most reliable for existing TP/SL orders or open positions.
216
+ - If an unfilled LIMIT/STOP order returns `Failed to update order`, wait for fill and use `set_tp_sl`.
183
217
 
184
218
  ### `adjust_margin`
185
219
  ```json
@@ -188,10 +222,22 @@ It includes:
188
222
  "arguments": {
189
223
  "poolId": "0xPOOL_ID",
190
224
  "positionId": "0xPOSITION_ID",
191
- "adjustAmount": "1000000"
225
+ "adjustAmount": "1"
192
226
  }
193
227
  }
194
228
  ```
229
+ Raw precision mode:
230
+ ```json
231
+ {
232
+ "name": "adjust_margin",
233
+ "arguments": {
234
+ "poolId": "0xPOOL_ID",
235
+ "positionId": "0xPOSITION_ID",
236
+ "adjustAmount": "raw:1000000"
237
+ }
238
+ }
239
+ ```
240
+ Success payload includes normalized raw amount under `data.normalized.adjustAmountRaw`.
195
241
 
196
242
  ### `check_approval`
197
243
  ```json
@@ -295,6 +341,7 @@ Note: this tool may internally fallback to SDK `api.getMarketList()` / `api.getP
295
341
  }
296
342
  }
297
343
  ```
344
+ Note: the server auto-retries with oracle/ticker market price when direct on-chain read hits divide-by-zero.
298
345
 
299
346
  ### `get_liquidity_info`
300
347
  ```json
@@ -502,6 +549,20 @@ If one section fails, check `data.meta.partial` and section-level `error` fields
502
549
  }
503
550
  }
504
551
  ```
552
+ Alias action example (`remove` == `withdraw`):
553
+ ```json
554
+ {
555
+ "name": "manage_liquidity",
556
+ "arguments": {
557
+ "action": "remove",
558
+ "poolType": "BASE",
559
+ "poolId": "0xPOOL_ID",
560
+ "amount": 1.5,
561
+ "slippage": 0.01
562
+ }
563
+ }
564
+ ```
565
+ If operation fails, check `error.message` for concrete reasons (for example `Insufficient Balance`) instead of generic `undefined`.
505
566
 
506
567
  ### `get_lp_price`
507
568
  ```json
@@ -26,9 +26,14 @@ 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
+ - **Adjust Margin**: \`adjust_margin.adjustAmount\` supports human amount (e.g. "1"), and also supports exact raw amount via \`raw:\` prefix.
35
+ - **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\`.
36
+ - **Pool Reads**: \`get_pool_info\` auto-retries with oracle/ticker price when direct on-chain read returns divide-by-zero.
32
37
  - **Examples**: Follow \`TOOL_EXAMPLES.md\` payload patterns when building tool arguments.
33
38
 
34
39
  ## 3. Self-Healing
package/dist/server.js CHANGED
@@ -11,6 +11,7 @@ import * as baseResources from "./resources/index.js";
11
11
  import * as basePrompts from "./prompts/index.js";
12
12
  import { logger } from "./utils/logger.js";
13
13
  import { MCPError, ErrorCode } from "./utils/errors.js";
14
+ import { extractErrorMessage, isMeaningfulErrorMessage } from "./utils/errorMessage.js";
14
15
  // --- Process Logic Protection ---
15
16
  // Catch unhandled promise rejections
16
17
  process.on('unhandledRejection', (reason, promise) => {
@@ -36,16 +37,6 @@ const allPrompts = Object.values(basePrompts);
36
37
  function safeJsonStringify(value) {
37
38
  return JSON.stringify(value, (_, item) => (typeof item === "bigint" ? item.toString() : item), 2);
38
39
  }
39
- function extractErrorMessage(raw) {
40
- if (raw instanceof Error)
41
- return raw.message;
42
- if (typeof raw === "string")
43
- return raw;
44
- if (raw && typeof raw === "object" && "message" in raw && typeof raw.message === "string") {
45
- return String(raw.message);
46
- }
47
- return String(raw ?? "Unknown error");
48
- }
49
40
  function inferToolErrorCode(message) {
50
41
  const lower = message.toLowerCase();
51
42
  if (lower.includes("required") || lower.includes("invalid") || lower.includes("must be") || lower.includes("unexpected") || lower.includes("unrecognized")) {
@@ -112,6 +103,102 @@ function parseFirstText(result) {
112
103
  const firstText = result.content.find((item) => item?.type === "text" && typeof item.text === "string");
113
104
  return firstText?.text ?? null;
114
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
+ }
115
202
  function normalizeToolErrorResult(toolName, result) {
116
203
  const rawText = parseFirstText(result);
117
204
  if (!rawText) {
@@ -134,14 +221,14 @@ function normalizeToolErrorResult(toolName, result) {
134
221
  parsed.status === "error" &&
135
222
  parsed.error &&
136
223
  typeof parsed.error.code === "string" &&
137
- typeof parsed.error.message === "string") {
224
+ isMeaningfulErrorMessage(parsed.error.message)) {
138
225
  return result;
139
226
  }
140
- const plainMessage = typeof parsed?.error?.message === "string"
227
+ const plainMessage = extractErrorMessage(typeof parsed?.error?.message === "string"
141
228
  ? parsed.error.message
142
229
  : typeof parsed?.message === "string"
143
230
  ? parsed.message
144
- : rawText.replace(/^Error:\s*/i, "").trim();
231
+ : rawText.replace(/^Error:\s*/i, "").trim(), `Tool "${toolName}" failed.`);
145
232
  const code = typeof parsed?.error?.code === "string"
146
233
  ? parsed.error.code
147
234
  : inferToolErrorCode(plainMessage);
@@ -217,27 +304,30 @@ function normalizeSdkReadFailure(toolName, result) {
217
304
  function zodSchemaToJsonSchema(zodSchema) {
218
305
  const toPropSchema = (value) => {
219
306
  const def = value?._def;
220
- const typeName = def?.typeName;
221
- if (typeName === "ZodOptional" || typeName === "ZodNullable") {
307
+ const zodType = def?.type;
308
+ if (zodType === "optional" || zodType === "nullable" || zodType === "default" || zodType === "prefault" || zodType === "catch") {
222
309
  return toPropSchema(def?.innerType);
223
310
  }
224
- if (typeName === "ZodString")
311
+ if (zodType === "pipe") {
312
+ return toPropSchema(def?.out);
313
+ }
314
+ if (zodType === "string")
225
315
  return { type: "string" };
226
- if (typeName === "ZodNumber")
316
+ if (zodType === "number")
227
317
  return { type: "number" };
228
- if (typeName === "ZodBoolean")
318
+ if (zodType === "boolean")
229
319
  return { type: "boolean" };
230
- if (typeName === "ZodArray") {
231
- return { type: "array", items: toPropSchema(def?.type) };
320
+ if (zodType === "array") {
321
+ return { type: "array", items: toPropSchema(def?.element) };
232
322
  }
233
- if (typeName === "ZodLiteral") {
323
+ if (zodType === "literal") {
234
324
  return { const: def?.value };
235
325
  }
236
- if (typeName === "ZodUnion") {
326
+ if (zodType === "union") {
237
327
  const options = def?.options || [];
238
328
  return { anyOf: options.map((opt) => toPropSchema(opt)) };
239
329
  }
240
- if (typeName === "ZodObject") {
330
+ if (zodType === "object") {
241
331
  return { type: "object" };
242
332
  }
243
333
  return { type: "string" };
@@ -246,7 +336,8 @@ function zodSchemaToJsonSchema(zodSchema) {
246
336
  const required = [];
247
337
  for (const [key, value] of Object.entries(zodSchema)) {
248
338
  const desc = value?._def?.description || "";
249
- const isOptional = value?.isOptional?.() || value?._def?.typeName === "ZodOptional";
339
+ const fieldType = value?._def?.type;
340
+ const isOptional = value?.isOptional?.() || fieldType === "optional" || fieldType === "default";
250
341
  const prop = toPropSchema(value);
251
342
  if (desc)
252
343
  prop.description = desc;
@@ -261,7 +352,7 @@ function zodSchemaToJsonSchema(zodSchema) {
261
352
  };
262
353
  }
263
354
  // ─── MCP Server ───
264
- const server = new Server({ name: "myx-mcp-trading-server", version: "3.0.0" }, { capabilities: { tools: {}, resources: {}, prompts: {} } });
355
+ const server = new Server({ name: "myx-mcp-trading-server", version: "3.0.2" }, { capabilities: { tools: {}, resources: {}, prompts: {} } });
265
356
  // List tools
266
357
  server.setRequestHandler(ListToolsRequestSchema, async () => {
267
358
  return {
@@ -288,8 +379,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
288
379
  try {
289
380
  let validatedArgs = args ?? {};
290
381
  if (tool.schema) {
382
+ const normalizedArgs = normalizeToolArgsBySchema(args ?? {}, tool.schema);
291
383
  try {
292
- validatedArgs = z.object(tool.schema).strict().parse(args ?? {});
384
+ validatedArgs = z.object(tool.schema).strict().parse(normalizedArgs);
293
385
  }
294
386
  catch (validationError) {
295
387
  if (validationError instanceof ZodError) {
@@ -381,7 +473,7 @@ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
381
473
  async function main() {
382
474
  const transport = new StdioServerTransport();
383
475
  await server.connect(transport);
384
- logger.info("🚀 MYX Trading MCP Server v3.0.0 running (stdio, pure on-chain, prod ready)");
476
+ logger.info("🚀 MYX Trading MCP Server v3.0.2 running (stdio, pure on-chain, prod ready)");
385
477
  }
386
478
  main().catch((err) => {
387
479
  logger.error("Fatal Server Startup Error", err);
@@ -1,5 +1,24 @@
1
1
  import { pool, quote, base } from "@myx-trade/sdk";
2
2
  import { getChainId, resolveClient } from "../auth/resolveClient.js";
3
+ import { extractErrorMessage } from "../utils/errorMessage.js";
4
+ import { ensureUnits } from "../utils/units.js";
5
+ function isDivideByZeroError(message) {
6
+ const lower = message.toLowerCase();
7
+ return (lower.includes("divide_by_zero") ||
8
+ lower.includes("division by zero") ||
9
+ lower.includes("panic code 0x12") ||
10
+ lower.includes("panic code: 0x12") ||
11
+ lower.includes("0x4e487b71"));
12
+ }
13
+ function toPositiveBigint(input) {
14
+ try {
15
+ const value = BigInt(String(input ?? "").trim());
16
+ return value > 0n ? value : null;
17
+ }
18
+ catch {
19
+ return null;
20
+ }
21
+ }
3
22
  /**
4
23
  * 创建合约市场池子
5
24
  */
@@ -11,9 +30,64 @@ export async function createPool(baseToken, marketId) {
11
30
  /**
12
31
  * 获取池子信息
13
32
  */
14
- export async function getPoolInfo(poolId, chainIdOverride) {
33
+ export async function getPoolInfo(poolId, chainIdOverride, clientOverride) {
15
34
  const chainId = chainIdOverride ?? getChainId();
16
- return pool.getPoolInfo(chainId, poolId);
35
+ let needOracleFallback = false;
36
+ try {
37
+ const direct = await pool.getPoolInfo(chainId, poolId);
38
+ if (direct)
39
+ return direct;
40
+ needOracleFallback = true;
41
+ }
42
+ catch (error) {
43
+ const message = extractErrorMessage(error);
44
+ if (!isDivideByZeroError(message)) {
45
+ throw new Error(`get_pool_info failed: ${message}`);
46
+ }
47
+ needOracleFallback = true;
48
+ }
49
+ if (!needOracleFallback)
50
+ return undefined;
51
+ const client = clientOverride ?? (await resolveClient()).client;
52
+ if (!client?.utils?.getOraclePrice) {
53
+ throw new Error("get_pool_info failed and oracle fallback is unavailable (client.utils.getOraclePrice missing).");
54
+ }
55
+ let oracleRaw = 0n;
56
+ try {
57
+ const oracle = await client.utils.getOraclePrice(poolId, chainId);
58
+ const byValue = toPositiveBigint(oracle?.value);
59
+ const byPrice = toPositiveBigint(oracle?.price);
60
+ oracleRaw = byValue ?? byPrice ?? 0n;
61
+ }
62
+ catch (error) {
63
+ throw new Error(`get_pool_info fallback failed to fetch oracle price: ${extractErrorMessage(error)}`);
64
+ }
65
+ if (oracleRaw <= 0n) {
66
+ try {
67
+ const tickerRes = await client.markets.getTickerList({ chainId, poolIds: [poolId] });
68
+ const row = Array.isArray(tickerRes) ? tickerRes[0] : tickerRes?.data?.[0];
69
+ if (row?.price) {
70
+ const tickerRaw = ensureUnits(row.price, 30, "ticker price");
71
+ const byTicker = toPositiveBigint(tickerRaw);
72
+ oracleRaw = byTicker ?? 0n;
73
+ }
74
+ }
75
+ catch {
76
+ }
77
+ }
78
+ if (oracleRaw <= 0n) {
79
+ throw new Error("get_pool_info fallback requires a positive oracle/ticker price, but both resolved to 0.");
80
+ }
81
+ try {
82
+ const retried = await pool.getPoolInfo(chainId, poolId, oracleRaw);
83
+ if (!retried) {
84
+ throw new Error(`Pool info for ${poolId} returned undefined after oracle-price retry.`);
85
+ }
86
+ return retried;
87
+ }
88
+ catch (error) {
89
+ throw new Error(`get_pool_info failed after oracle-price retry: ${extractErrorMessage(error)}`);
90
+ }
17
91
  }
18
92
  /**
19
93
  * 获取池子详情
@@ -1,9 +1,10 @@
1
1
  import { Direction, OrderType, TriggerType } from "@myx-trade/sdk";
2
- import { getChainId, getQuoteToken } from "../auth/resolveClient.js";
2
+ import { getChainId, getQuoteToken, getQuoteDecimals } from "../auth/resolveClient.js";
3
3
  import { ensureUnits } from "../utils/units.js";
4
4
  import { normalizeAddress } from "../utils/address.js";
5
5
  import { normalizeSlippagePct4dp } from "../utils/slippage.js";
6
6
  import { finalizeMutationResult } from "../utils/mutationResult.js";
7
+ import { extractErrorMessage } from "../utils/errorMessage.js";
7
8
  function resolveDirection(direction) {
8
9
  if (direction !== 0 && direction !== 1) {
9
10
  throw new Error("direction must be 0 (LONG) or 1 (SHORT).");
@@ -51,6 +52,74 @@ function resolveTpSlTriggerType(isTp, direction, triggerType) {
51
52
  return direction === 0 ? 2 : 1;
52
53
  }
53
54
  }
55
+ function collectRows(input) {
56
+ if (Array.isArray(input))
57
+ return input.flatMap(collectRows);
58
+ if (!input || typeof input !== "object")
59
+ return [];
60
+ if (input.poolId || input.pool_id || input.marketId || input.market_id)
61
+ return [input];
62
+ return Object.values(input).flatMap(collectRows);
63
+ }
64
+ function parseDecimals(value, fallback) {
65
+ const parsed = Number(value);
66
+ if (Number.isFinite(parsed) && parsed >= 0) {
67
+ return Math.floor(parsed);
68
+ }
69
+ return fallback;
70
+ }
71
+ function normalizeIdentifier(value) {
72
+ return String(value ?? "").trim().toLowerCase();
73
+ }
74
+ async function resolveDecimalsForUpdateOrder(client, chainId, marketId, poolIdHint) {
75
+ let baseDecimals = 18;
76
+ let quoteDecimals = getQuoteDecimals();
77
+ let resolvedPoolId = String(poolIdHint ?? "").trim();
78
+ const hydrateFromPoolDetail = async (poolId) => {
79
+ if (!poolId)
80
+ return;
81
+ const detailRes = await client.markets.getMarketDetail({ chainId, poolId });
82
+ const detail = detailRes?.data || (detailRes?.marketId ? detailRes : null);
83
+ if (!detail)
84
+ return;
85
+ baseDecimals = parseDecimals(detail.baseDecimals, baseDecimals);
86
+ quoteDecimals = parseDecimals(detail.quoteDecimals, quoteDecimals);
87
+ resolvedPoolId = String(detail.poolId ?? poolId ?? resolvedPoolId);
88
+ };
89
+ if (resolvedPoolId) {
90
+ try {
91
+ await hydrateFromPoolDetail(resolvedPoolId);
92
+ return { baseDecimals, quoteDecimals, poolId: resolvedPoolId };
93
+ }
94
+ catch {
95
+ resolvedPoolId = "";
96
+ }
97
+ }
98
+ try {
99
+ const marketListRes = await client.api.getMarketList();
100
+ const rows = collectRows(marketListRes?.data ?? marketListRes);
101
+ const targetMarketId = normalizeIdentifier(marketId);
102
+ const row = rows.find((item) => normalizeIdentifier(item?.marketId ?? item?.market_id) === targetMarketId);
103
+ if (row) {
104
+ baseDecimals = parseDecimals(row?.baseDecimals ?? row?.base_decimals, baseDecimals);
105
+ quoteDecimals = parseDecimals(row?.quoteDecimals ?? row?.quote_decimals, quoteDecimals);
106
+ const fromRowPoolId = String(row?.poolId ?? row?.pool_id ?? "").trim();
107
+ if (fromRowPoolId) {
108
+ resolvedPoolId = fromRowPoolId;
109
+ }
110
+ }
111
+ }
112
+ catch {
113
+ }
114
+ if (resolvedPoolId) {
115
+ try {
116
+ await hydrateFromPoolDetail(resolvedPoolId);
117
+ }
118
+ catch {
119
+ }
120
+ }
121
+ return { baseDecimals, quoteDecimals, poolId: resolvedPoolId || undefined };
122
+ }
54
123
  /**
55
124
  * 开仓 / 加仓
56
125
  */
@@ -67,7 +136,7 @@ export async function openPosition(client, address, args) {
67
136
  }
68
137
  const baseDecimals = poolData.baseDecimals || 18;
69
138
  const quoteDecimals = poolData.quoteDecimals || 6;
70
- const collateralRaw = ensureUnits(args.collateralAmount, quoteDecimals, "collateralAmount");
139
+ const collateralRaw = ensureUnits(args.collateralAmount, quoteDecimals, "collateralAmount", { allowImplicitRaw: false });
71
140
  // --- Pre-flight Check: minOrderSizeInUsd ---
72
141
  try {
73
142
  const levelRes = await client.markets.getPoolLevelConfig(args.poolId, chainId);
@@ -94,9 +163,9 @@ export async function openPosition(client, address, args) {
94
163
  throw e;
95
164
  console.warn(`[tradeService] Limit check skipped: ${e.message}`);
96
165
  }
97
- const sizeRaw = ensureUnits(args.size, baseDecimals, "size");
98
- const priceRaw = ensureUnits(args.price, 30, "price");
99
- 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 });
100
169
  // --- Auto-Deposit Logic (Strict) ---
101
170
  const allowAutoDeposit = args.autoDeposit !== false;
102
171
  console.log(`[tradeService] Checking marginBalance for ${address} in pool ${args.poolId}...`);
@@ -158,13 +227,13 @@ export async function openPosition(client, address, args) {
158
227
  leverage: args.leverage,
159
228
  };
160
229
  if (args.tpSize)
161
- orderParams.tpSize = ensureUnits(args.tpSize, baseDecimals, "tpSize");
230
+ orderParams.tpSize = ensureUnits(args.tpSize, baseDecimals, "tpSize", { allowImplicitRaw: false });
162
231
  if (args.tpPrice)
163
- orderParams.tpPrice = ensureUnits(args.tpPrice, 30, "tpPrice");
232
+ orderParams.tpPrice = ensureUnits(args.tpPrice, 30, "tpPrice", { allowImplicitRaw: false });
164
233
  if (args.slSize)
165
- orderParams.slSize = ensureUnits(args.slSize, baseDecimals, "slSize");
234
+ orderParams.slSize = ensureUnits(args.slSize, baseDecimals, "slSize", { allowImplicitRaw: false });
166
235
  if (args.slPrice)
167
- orderParams.slPrice = ensureUnits(args.slPrice, 30, "slPrice");
236
+ orderParams.slPrice = ensureUnits(args.slPrice, 30, "slPrice", { allowImplicitRaw: false });
168
237
  return client.order.createIncreaseOrder(orderParams, tradingFeeRaw, args.marketId);
169
238
  }
170
239
  /**
@@ -194,9 +263,9 @@ export async function closePosition(client, address, args) {
194
263
  orderType: args.orderType,
195
264
  triggerType: resolveTriggerType(args.orderType, args.direction, true, args.triggerType),
196
265
  direction: dir,
197
- collateralAmount: ensureUnits(args.collateralAmount, quoteDecimals, "collateralAmount"),
198
- size: ensureUnits(args.size, baseDecimals, "size"),
199
- 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 }),
200
269
  timeInForce: args.timeInForce,
201
270
  postOnly: args.postOnly,
202
271
  slippagePct: normalizeSlippagePct4dp(args.slippagePct),
@@ -234,13 +303,13 @@ export async function setPositionTpSl(client, address, args) {
234
303
  slippagePct: normalizeSlippagePct4dp(args.slippagePct),
235
304
  };
236
305
  if (args.tpPrice)
237
- params.tpPrice = ensureUnits(args.tpPrice, 30, "tpPrice");
306
+ params.tpPrice = ensureUnits(args.tpPrice, 30, "tpPrice", { allowImplicitRaw: false });
238
307
  if (args.tpSize)
239
- params.tpSize = ensureUnits(args.tpSize, baseDecimals, "tpSize");
308
+ params.tpSize = ensureUnits(args.tpSize, baseDecimals, "tpSize", { allowImplicitRaw: false });
240
309
  if (args.slPrice)
241
- params.slPrice = ensureUnits(args.slPrice, 30, "slPrice");
310
+ params.slPrice = ensureUnits(args.slPrice, 30, "slPrice", { allowImplicitRaw: false });
242
311
  if (args.slSize)
243
- params.slSize = ensureUnits(args.slSize, baseDecimals, "slSize");
312
+ params.slSize = ensureUnits(args.slSize, baseDecimals, "slSize", { allowImplicitRaw: false });
244
313
  return client.order.createPositionTpSlOrder(params);
245
314
  }
246
315
  /**
@@ -249,9 +318,25 @@ export async function setPositionTpSl(client, address, args) {
249
318
  export async function adjustMargin(client, address, args) {
250
319
  const chainId = getChainId();
251
320
  const quoteToken = normalizeAddress(args.quoteToken || getQuoteToken(), "quoteToken");
252
- const adjustAmount = String(args.adjustAmount ?? "").trim();
321
+ const adjustAmountInput = String(args.adjustAmount ?? "").trim();
322
+ if (!adjustAmountInput) {
323
+ throw new Error("adjustAmount is required.");
324
+ }
325
+ let quoteDecimals = getQuoteDecimals();
326
+ try {
327
+ const detailRes = await client.markets.getMarketDetail({ chainId, poolId: args.poolId });
328
+ const detail = detailRes?.data || (detailRes?.marketId ? detailRes : null);
329
+ const parsed = Number(detail?.quoteDecimals);
330
+ if (Number.isFinite(parsed) && parsed >= 0) {
331
+ quoteDecimals = parsed;
332
+ }
333
+ }
334
+ catch {
335
+ // Fallback to env quote decimals if market detail is unavailable.
336
+ }
337
+ const adjustAmount = ensureUnits(adjustAmountInput, quoteDecimals, "adjustAmount", { allowImplicitRaw: false });
253
338
  if (!/^-?\d+$/.test(adjustAmount)) {
254
- throw new Error("adjustAmount must be an integer string in quote token raw units.");
339
+ throw new Error("adjustAmount must resolve to an integer string (raw units).");
255
340
  }
256
341
  const params = {
257
342
  poolId: args.poolId,
@@ -264,7 +349,14 @@ export async function adjustMargin(client, address, args) {
264
349
  if (args.poolOracleType !== undefined) {
265
350
  params.poolOracleType = Number(args.poolOracleType);
266
351
  }
267
- 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;
268
360
  }
269
361
  /**
270
362
  * 平掉所有仓位
@@ -319,16 +411,36 @@ export async function closeAllPositions(client, address) {
319
411
  export async function updateOrderTpSl(client, address, args) {
320
412
  const chainId = getChainId();
321
413
  const quoteToken = normalizeAddress(args.quoteToken, "quoteToken");
414
+ const marketId = String(args.marketId ?? "").trim();
415
+ const isTpSlOrder = typeof args.isTpSlOrder === "boolean" ? args.isTpSlOrder : true;
416
+ const { baseDecimals } = await resolveDecimalsForUpdateOrder(client, chainId, marketId, args.poolId);
322
417
  const params = {
323
418
  orderId: args.orderId,
324
- tpSize: args.tpSize,
325
- tpPrice: args.tpPrice,
326
- slSize: args.slSize,
327
- slPrice: args.slPrice,
328
- useOrderCollateral: args.useOrderCollateral,
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 }),
423
+ useOrderCollateral: Boolean(args.useOrderCollateral),
329
424
  executionFeeToken: quoteToken,
330
- size: args.size,
331
- price: args.price,
425
+ size: ensureUnits(args.size, baseDecimals, "size", { allowImplicitRaw: false }),
426
+ price: ensureUnits(args.price, 30, "price", { allowImplicitRaw: false }),
332
427
  };
333
- return client.order.updateOrderTpSl(params, quoteToken, chainId, address, args.marketId, args.isTpSlOrder);
428
+ try {
429
+ const result = await client.order.updateOrderTpSl(params, quoteToken, chainId, address, marketId, isTpSlOrder);
430
+ if (Number(result?.code) === 0) {
431
+ return result;
432
+ }
433
+ const message = extractErrorMessage(result, "Failed to update order");
434
+ if (/failed to update order/i.test(message)) {
435
+ throw new Error(`Failed to update TP/SL for order ${args.orderId}. If this is a pending LIMIT/STOP order, wait for fill and then use set_tp_sl on the position.`);
436
+ }
437
+ throw new Error(`update_order_tp_sl failed: ${message}`);
438
+ }
439
+ catch (error) {
440
+ const message = extractErrorMessage(error, "Failed to update order");
441
+ if (/failed to update order/i.test(message)) {
442
+ throw new Error(`Failed to update TP/SL for order ${args.orderId}. If this is a pending LIMIT/STOP order, wait for fill and then use set_tp_sl on the position.`);
443
+ }
444
+ throw new Error(message);
445
+ }
334
446
  }
@@ -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,13 +2,14 @@ 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.",
8
9
  schema: {
9
10
  poolId: z.string().describe("Pool ID"),
10
11
  positionId: z.string().describe("Position ID"),
11
- adjustAmount: z.coerce.string().regex(/^-?\d+$/).describe("Quote token raw units. Positive = add, negative = remove."),
12
+ adjustAmount: z.union([z.string(), z.number()]).describe("Adjust amount. Human units are supported (e.g. '1' = 1 USDC). Use 'raw:<int>' for exact raw units."),
12
13
  quoteToken: z.string().optional().describe("Quote token address"),
13
14
  poolOracleType: z.coerce.number().optional().describe("Oracle type: 1 for Chainlink, 2 for Pyth"),
14
15
  },
@@ -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
  };
@@ -1,17 +1,49 @@
1
1
  import { z } from "zod";
2
2
  import { resolveClient, getChainId } from "../auth/resolveClient.js";
3
3
  import { finalizeMutationResult } from "../utils/mutationResult.js";
4
+ function normalizeOrderIds(input) {
5
+ if (Array.isArray(input)) {
6
+ return input.map((id) => String(id).trim()).filter(Boolean);
7
+ }
8
+ if (typeof input === "string") {
9
+ const text = input.trim();
10
+ if (!text)
11
+ return [];
12
+ // Support toolchains that serialize arrays as JSON strings.
13
+ if (text.startsWith("[") && text.endsWith("]")) {
14
+ try {
15
+ const parsed = JSON.parse(text);
16
+ if (Array.isArray(parsed)) {
17
+ return parsed.map((id) => String(id).trim()).filter(Boolean);
18
+ }
19
+ }
20
+ catch {
21
+ // Fallback to comma/single parsing below.
22
+ }
23
+ }
24
+ if (text.includes(",")) {
25
+ return text.split(",").map((id) => id.trim()).filter(Boolean);
26
+ }
27
+ return [text];
28
+ }
29
+ return [];
30
+ }
4
31
  export const cancelAllOrdersTool = {
5
32
  name: "cancel_all_orders",
6
- description: "Cancel multiple open orders by orderIds.",
33
+ description: "Cancel open orders by orderIds. Accepts array, JSON-array string, comma-separated string, or single orderId.",
7
34
  schema: {
8
- orderIds: z.array(z.string()).min(1).describe("Order IDs to cancel."),
35
+ orderIds: z
36
+ .union([z.array(z.string()).min(1), z.string().min(1)])
37
+ .describe("Order IDs to cancel. Supports array or string (single/comma/JSON-array)."),
9
38
  },
10
39
  handler: async (args) => {
11
40
  try {
12
41
  const { client, signer } = await resolveClient();
13
42
  const chainId = getChainId();
14
- const orderIds = args.orderIds;
43
+ const orderIds = normalizeOrderIds(args.orderIds);
44
+ if (orderIds.length === 0) {
45
+ throw new Error("orderIds is required and must include at least one non-empty order id.");
46
+ }
15
47
  const raw = await client.order.cancelAllOrders(orderIds, chainId);
16
48
  const result = await finalizeMutationResult(raw, signer, "cancel_all_orders");
17
49
  return {
@@ -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."),
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)."),
33
38
  marketId: z.string().describe("Specific Market Config Hash. Fetch via get_market_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
  };
@@ -3,6 +3,7 @@ import { quoteDeposit, quoteWithdraw, baseDeposit, baseWithdraw, getLpPrice, } f
3
3
  import { resolveClient } from "../auth/resolveClient.js";
4
4
  import { resolvePool } from "../services/marketService.js";
5
5
  import { finalizeMutationResult } from "../utils/mutationResult.js";
6
+ import { extractErrorMessage } from "../utils/errorMessage.js";
6
7
  export const manageLiquidityTool = {
7
8
  name: "manage_liquidity",
8
9
  description: "Add or withdraw liquidity from a BASE or QUOTE pool.",
@@ -40,11 +41,14 @@ export const manageLiquidityTool = {
40
41
  if (!raw) {
41
42
  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.`);
42
43
  }
44
+ if (raw && typeof raw === "object" && "code" in raw && Number(raw.code) !== 0) {
45
+ throw new Error(`Liquidity ${action} failed: ${extractErrorMessage(raw)}`);
46
+ }
43
47
  const data = await finalizeMutationResult(raw, signer, "manage_liquidity");
44
48
  return { content: [{ type: "text", text: JSON.stringify({ status: "success", data }, (_, v) => typeof v === 'bigint' ? v.toString() : v, 2) }] };
45
49
  }
46
50
  catch (error) {
47
- return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
51
+ return { content: [{ type: "text", text: `Error: ${extractErrorMessage(error)}` }], isError: true };
48
52
  }
49
53
  },
50
54
  };
@@ -62,7 +66,7 @@ export const getLpPriceTool = {
62
66
  return { content: [{ type: "text", text: JSON.stringify({ status: "success", data }, (k, v) => typeof v === 'bigint' ? v.toString() : v, 2) }] };
63
67
  }
64
68
  catch (error) {
65
- return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
69
+ return { content: [{ type: "text", text: `Error: ${extractErrorMessage(error)}` }], isError: true };
66
70
  }
67
71
  },
68
72
  };
@@ -2,6 +2,7 @@ import { z } from "zod";
2
2
  import { resolveClient, getChainId } from "../auth/resolveClient.js";
3
3
  import { getMarketDetail, resolvePool } from "../services/marketService.js";
4
4
  import { getPoolInfo, getLiquidityInfo } from "../services/poolService.js";
5
+ import { extractErrorMessage } from "../utils/errorMessage.js";
5
6
  export const getMarketDetailTool = {
6
7
  name: "get_market_detail",
7
8
  description: "Get detailed information for a specific trading pool (fee rates, open interest, etc.). Supports PoolId, Token Address, or Keywords.",
@@ -20,7 +21,7 @@ export const getMarketDetailTool = {
20
21
  return { content: [{ type: "text", text: JSON.stringify({ status: "success", data }, (_, v) => typeof v === 'bigint' ? v.toString() : v, 2) }] };
21
22
  }
22
23
  catch (error) {
23
- return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
24
+ return { content: [{ type: "text", text: `Error: ${extractErrorMessage(error)}` }], isError: true };
24
25
  }
25
26
  },
26
27
  };
@@ -36,13 +37,13 @@ export const getPoolInfoTool = {
36
37
  const { client } = await resolveClient();
37
38
  const poolId = await resolvePool(client, args.poolId);
38
39
  const chainId = args.chainId ?? getChainId();
39
- const data = await getPoolInfo(poolId, chainId);
40
+ const data = await getPoolInfo(poolId, chainId, client);
40
41
  if (!data)
41
42
  throw new Error(`Pool info for ${poolId} returned undefined. The pool may not exist on chainId ${chainId}.`);
42
43
  return { content: [{ type: "text", text: JSON.stringify({ status: "success", data }, (_, v) => typeof v === 'bigint' ? v.toString() : v, 2) }] };
43
44
  }
44
45
  catch (error) {
45
- return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
46
+ return { content: [{ type: "text", text: `Error: ${extractErrorMessage(error)}` }], isError: true };
46
47
  }
47
48
  },
48
49
  };
@@ -65,7 +66,7 @@ export const getLiquidityInfoTool = {
65
66
  return { content: [{ type: "text", text: JSON.stringify({ status: "success", data }, (_, v) => typeof v === 'bigint' ? v.toString() : v, 2) }] };
66
67
  }
67
68
  catch (error) {
68
- return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
69
+ return { content: [{ type: "text", text: `Error: ${extractErrorMessage(error)}` }], isError: true };
69
70
  }
70
71
  },
71
72
  };
@@ -2,20 +2,22 @@ import { z } from "zod";
2
2
  import { resolveClient } from "../auth/resolveClient.js";
3
3
  import { updateOrderTpSl } from "../services/tradeService.js";
4
4
  import { finalizeMutationResult } from "../utils/mutationResult.js";
5
+ import { extractErrorMessage } from "../utils/errorMessage.js";
5
6
  export const updateOrderTpSlTool = {
6
7
  name: "update_order_tp_sl",
7
8
  description: "Update an existing take profit or stop loss order.",
8
9
  schema: {
9
10
  orderId: z.string().describe("The ID of the order to update"),
10
11
  marketId: z.string().describe("The market ID (config hash) for the order"),
11
- size: z.string().describe("Order size (raw or human-readable)"),
12
- price: z.string().describe("Order price (raw or human-readable, 30 decimals)"),
13
- tpPrice: z.string().describe("TP price (raw or human-readable, 30 decimals)"),
14
- tpSize: z.string().describe("TP size (raw or human-readable)"),
15
- slPrice: z.string().describe("SL price (raw or human-readable, 30 decimals)"),
16
- slSize: z.string().describe("SL size (raw or human-readable)"),
17
- useOrderCollateral: z.boolean().describe("Whether to use order collateral"),
18
- isTpSlOrder: z.boolean().optional().describe("Whether this is a TP/SL order"),
12
+ poolId: z.string().optional().describe("Optional poolId hint for decimal resolution."),
13
+ size: z.union([z.string(), z.number()]).describe("Order size (raw or human-readable)"),
14
+ price: z.union([z.string(), z.number()]).describe("Order price (raw or human-readable, 30 decimals)"),
15
+ tpPrice: z.union([z.string(), z.number()]).describe("TP price (raw or human-readable, 30 decimals)"),
16
+ tpSize: z.union([z.string(), z.number()]).describe("TP size (raw or human-readable)"),
17
+ slPrice: z.union([z.string(), z.number()]).describe("SL price (raw or human-readable, 30 decimals)"),
18
+ slSize: z.union([z.string(), z.number()]).describe("SL size (raw or human-readable)"),
19
+ useOrderCollateral: z.coerce.boolean().describe("Whether to use order collateral"),
20
+ isTpSlOrder: z.coerce.boolean().optional().describe("Whether this is a TP/SL order"),
19
21
  quoteToken: z.string().describe("Quote token address"),
20
22
  },
21
23
  handler: async (args) => {
@@ -26,7 +28,7 @@ export const updateOrderTpSlTool = {
26
28
  return { content: [{ type: "text", text: JSON.stringify({ status: "success", data }, (_, v) => typeof v === 'bigint' ? v.toString() : v, 2) }] };
27
29
  }
28
30
  catch (error) {
29
- return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
31
+ return { content: [{ type: "text", text: `Error: ${extractErrorMessage(error)}` }], isError: true };
30
32
  }
31
33
  },
32
34
  };
@@ -0,0 +1,69 @@
1
+ const USELESS_MESSAGE_SET = new Set([
2
+ "",
3
+ "undefined",
4
+ "null",
5
+ "[object object]",
6
+ "{}",
7
+ ]);
8
+ function cleanMessage(input) {
9
+ if (typeof input !== "string")
10
+ return null;
11
+ const text = input.trim();
12
+ if (!text)
13
+ return null;
14
+ if (USELESS_MESSAGE_SET.has(text.toLowerCase()))
15
+ return null;
16
+ return text;
17
+ }
18
+ function isRecord(value) {
19
+ return !!value && typeof value === "object";
20
+ }
21
+ function safeStringify(value) {
22
+ try {
23
+ return cleanMessage(JSON.stringify(value, (_, item) => (typeof item === "bigint" ? item.toString() : item)));
24
+ }
25
+ catch {
26
+ return cleanMessage(String(value ?? ""));
27
+ }
28
+ }
29
+ export function isMeaningfulErrorMessage(message) {
30
+ return cleanMessage(message) !== null;
31
+ }
32
+ export function extractErrorMessage(error, fallback = "Unknown error") {
33
+ const visited = new Set();
34
+ const read = (value, depth) => {
35
+ if (depth > 6 || value === null || value === undefined)
36
+ return null;
37
+ if (typeof value === "string")
38
+ return cleanMessage(value);
39
+ if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
40
+ return cleanMessage(String(value));
41
+ }
42
+ if (value instanceof Error) {
43
+ const message = cleanMessage(value.message);
44
+ if (message)
45
+ return message;
46
+ return read(value.cause, depth + 1);
47
+ }
48
+ if (!isRecord(value)) {
49
+ return cleanMessage(String(value));
50
+ }
51
+ if (visited.has(value))
52
+ return null;
53
+ visited.add(value);
54
+ const directKeys = ["message", "reason", "shortMessage", "msg", "detail", "error_description"];
55
+ for (const key of directKeys) {
56
+ const message = read(value[key], depth + 1);
57
+ if (message)
58
+ return message;
59
+ }
60
+ const nestedKeys = ["error", "data", "cause", "response", "info"];
61
+ for (const key of nestedKeys) {
62
+ const message = read(value[key], depth + 1);
63
+ if (message)
64
+ return message;
65
+ }
66
+ return safeStringify(value);
67
+ };
68
+ return read(error, 0) ?? fallback;
69
+ }
@@ -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.0",
3
+ "version": "3.0.2",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "myx-mcp": "dist/server.js"