@michaleffffff/mcp-trading-server 3.0.1 → 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,17 @@
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
+
3
15
  ## 3.0.1 - 2026-03-17
4
16
 
5
17
  ### Added
package/README.md CHANGED
@@ -291,7 +291,7 @@ 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.
@@ -377,6 +377,12 @@ Use this query/mutation loop for limit-order management:
377
377
  4. Check historical fills/cancellations with `get_order_history`.
378
378
  5. Cancel one order via `cancel_order` or batch via `cancel_all_orders`.
379
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.
385
+
380
386
  Minimal query examples:
381
387
 
382
388
  ```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
@@ -26,8 +26,10 @@ 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).
32
34
  - **Adjust Margin**: \`adjust_margin.adjustAmount\` supports human amount (e.g. "1"), and also supports exact raw amount via \`raw:\` prefix.
33
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\`.
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.2" }, { 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.2 running (stdio, pure on-chain, prod ready)");
376
477
  }
377
478
  main().catch((err) => {
378
479
  logger.error("Fatal Server Startup Error", err);
@@ -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
  };
@@ -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
  };
@@ -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.2",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "myx-mcp": "dist/server.js"