@michaleffffff/mcp-trading-server 3.0.20 → 3.0.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,6 +7,7 @@ import { normalizeSlippagePct4dp } from "../utils/slippage.js";
7
7
  import { finalizeMutationResult } from "../utils/mutationResult.js";
8
8
  import { extractErrorMessage } from "../utils/errorMessage.js";
9
9
  import { mapTimeInForce } from "../utils/mappings.js";
10
+ import { getFreshOraclePrice } from "./marketService.js";
10
11
  function resolveDirection(direction) {
11
12
  if (typeof direction === "string") {
12
13
  const text = direction.trim().toUpperCase();
@@ -154,6 +155,37 @@ function validateIncreaseOrderEconomics(args) {
154
155
  const priceHuman = formatUnits(priceRawBig, 30);
155
156
  throw new Error(`Invalid size semantics: size is BASE quantity, not USD notional. collateralAmount*leverage implies ≈${targetHuman} quote, but size*price implies ≈${actualHuman} quote. At price ${priceHuman}, recommended size is ≈${recommendedSizeHuman}.`);
156
157
  }
158
+ function countAdditionalExecutionOrders(args) {
159
+ let count = 1;
160
+ if (String(args.tpPrice ?? "").trim()) {
161
+ count += 1;
162
+ }
163
+ if (String(args.slPrice ?? "").trim()) {
164
+ count += 1;
165
+ }
166
+ return count;
167
+ }
168
+ async function getRequiredIncreaseSpendRaw(client, marketId, args, chainId) {
169
+ const collateralRaw = BigInt(args.collateralRaw);
170
+ const tradingFeeRaw = BigInt(args.tradingFeeRaw);
171
+ const executionOrderCount = countAdditionalExecutionOrders(args);
172
+ const networkFeeText = String(await client.utils.getNetworkFee(marketId, chainId) ?? "").trim();
173
+ if (!/^\d+$/.test(networkFeeText)) {
174
+ throw new Error(`Failed to resolve networkFee for marketId=${marketId}.`);
175
+ }
176
+ const baseNetworkFeeRaw = BigInt(networkFeeText);
177
+ if (baseNetworkFeeRaw <= 0n) {
178
+ throw new Error(`networkFee must be > 0 for marketId=${marketId}.`);
179
+ }
180
+ const networkFeeRaw = baseNetworkFeeRaw * BigInt(executionOrderCount);
181
+ return {
182
+ collateralRaw,
183
+ tradingFeeRaw,
184
+ networkFeeRaw,
185
+ executionOrderCount,
186
+ totalSpendRaw: collateralRaw + tradingFeeRaw + networkFeeRaw,
187
+ };
188
+ }
157
189
  async function resolveDecimalsForUpdateOrder(client, chainId, marketId, poolIdHint) {
158
190
  let baseDecimals = 18;
159
191
  let quoteDecimals = getQuoteDecimals();
@@ -249,6 +281,18 @@ export async function openPosition(client, address, args) {
249
281
  const sizeRaw = ensureUnits(args.size, baseDecimals, "size", { allowImplicitRaw: false });
250
282
  const priceRaw = ensureUnits(args.price, 30, "price", { allowImplicitRaw: false });
251
283
  const tradingFeeRaw = ensureUnits(args.tradingFee, quoteDecimals, "tradingFee", { allowImplicitRaw: false });
284
+ const resolvedMarketId = String(args.marketId ?? poolData.marketId ?? "").trim();
285
+ if (!resolvedMarketId) {
286
+ throw new Error(`marketId is required to compute networkFee for poolId=${args.poolId}.`);
287
+ }
288
+ const spend = await getRequiredIncreaseSpendRaw(client, resolvedMarketId, {
289
+ collateralRaw,
290
+ tradingFeeRaw,
291
+ tpPrice: args.tpPrice,
292
+ tpSize: args.tpSize,
293
+ slPrice: args.slPrice,
294
+ slSize: args.slSize,
295
+ }, chainId);
252
296
  validateIncreaseOrderEconomics({
253
297
  collateralRaw,
254
298
  sizeRaw,
@@ -268,7 +312,7 @@ export async function openPosition(client, address, args) {
268
312
  marginBalanceRaw = toBigIntOrZero(marginInfo.data.freeMargin);
269
313
  walletBalanceRaw = toBigIntOrZero(marginInfo.data.walletBalance);
270
314
  }
271
- const requiredRaw = BigInt(collateralRaw);
315
+ const requiredRaw = spend.totalSpendRaw;
272
316
  if (marginBalanceRaw < requiredRaw) {
273
317
  if (!allowAutoDeposit) {
274
318
  throw new Error(`Insufficient marginBalance (${marginBalanceRaw.toString()}) for required collateral (${requiredRaw.toString()}). ` +
@@ -459,9 +503,14 @@ export async function closeAllPositions(client, address) {
459
503
  const results = [];
460
504
  for (const pos of positions) {
461
505
  const dir = pos.direction === 0 ? Direction.LONG : Direction.SHORT;
462
- // Get current price for slippage protection
463
- const oracleData = await client.utils.getOraclePrice(pos.poolId, chainId);
464
- const currentPrice30 = ensureUnits(oracleData.price, 30, "oracle price");
506
+ const marketDetailRes = await client.markets.getMarketDetail({ chainId, poolId: pos.poolId });
507
+ const marketDetail = marketDetailRes?.data || (marketDetailRes?.marketId ? marketDetailRes : null);
508
+ if (!marketDetail?.marketId) {
509
+ throw new Error(`Could not resolve market metadata for poolId=${pos.poolId}.`);
510
+ }
511
+ const baseDecimals = Number(marketDetail.baseDecimals ?? 18);
512
+ const oracleData = await getFreshOraclePrice(client, pos.poolId, chainId);
513
+ const currentPrice30 = ensureUnits(oracleData.price, 30, "oracle price", { allowImplicitRaw: false });
465
514
  // For LONG close (Decrease LONG): Price should be lower (e.g. 90% of current)
466
515
  // For SHORT close (Decrease SHORT): Price should be higher (e.g. 110% of current)
467
516
  // Here we use a safe 10% slippage price
@@ -472,7 +521,13 @@ export async function closeAllPositions(client, address) {
472
521
  else {
473
522
  slippagePrice30 = (BigInt(currentPrice30) * 110n) / 100n;
474
523
  }
475
- const sizeWei = ensureUnits(pos.size, 18, "size");
524
+ const sizeInput = /^\d+$/.test(String(pos.sizeRaw ?? pos.positionSizeRaw ?? "").trim())
525
+ ? `raw:${String(pos.sizeRaw ?? pos.positionSizeRaw).trim()}`
526
+ : String(pos.size ?? pos.positionSize ?? "").trim();
527
+ if (!sizeInput) {
528
+ throw new Error(`Position size missing for positionId=${String(pos.positionId ?? "").trim()}.`);
529
+ }
530
+ const sizeWei = ensureUnits(sizeInput, baseDecimals, "size", { allowImplicitRaw: false });
476
531
  const res = await client.order.createDecreaseOrder({
477
532
  chainId,
478
533
  address,
@@ -485,7 +540,7 @@ export async function closeAllPositions(client, address) {
485
540
  size: sizeWei,
486
541
  price: slippagePrice30.toString(),
487
542
  postOnly: false,
488
- slippagePct: "1000", // 10%
543
+ slippagePct: "100", // 1%
489
544
  executionFeeToken: getQuoteToken(),
490
545
  leverage: pos.userLeverage,
491
546
  });
@@ -2,6 +2,7 @@ import { z } from "zod";
2
2
  import { resolveClient, getChainId, getQuoteToken } from "../auth/resolveClient.js";
3
3
  import { normalizeAddress } from "../utils/address.js";
4
4
  import { finalizeMutationResult } from "../utils/mutationResult.js";
5
+ import { fetchErc20Decimals } from "../utils/token.js";
5
6
  const MAX_UINT256 = "115792089237316195423570985008687907853269984665640564039457584007913129639935";
6
7
  function asBigintOrNull(value) {
7
8
  try {
@@ -52,11 +53,9 @@ export const accountDepositTool = {
52
53
  const chainId = getChainId();
53
54
  const tokenAddressInput = String(args.tokenAddress ?? "").trim() || getQuoteToken();
54
55
  const tokenAddress = normalizeAddress(tokenAddressInput, "tokenAddress");
55
- // For deposit, we default to quote decimals (6) as it's the most common use case.
56
- // ensureUnits handles 'raw:' prefix if absolute precision is needed.
57
56
  const { ensureUnits } = await import("../utils/units.js");
58
- const { getQuoteDecimals } = await import("../auth/resolveClient.js");
59
- const amount = ensureUnits(args.amount, getQuoteDecimals(), "amount", { allowImplicitRaw: false });
57
+ const tokenDecimals = await fetchErc20Decimals(signer.provider ?? signer, tokenAddress, "deposit token");
58
+ const amount = ensureUnits(args.amount, tokenDecimals, "amount", { allowImplicitRaw: false });
60
59
  let approval = null;
61
60
  const needApproval = await client.utils.needsApproval(address, chainId, tokenAddress, amount);
62
61
  if (needApproval) {
@@ -102,9 +101,17 @@ export const accountWithdrawTool = {
102
101
  const { client, address, signer } = await resolveClient();
103
102
  const chainId = getChainId();
104
103
  const { ensureUnits } = await import("../utils/units.js");
105
- const { getQuoteDecimals } = await import("../auth/resolveClient.js");
106
- // Assuming 18 decimals for base and quoteDecimals for quote
107
- const decimals = args.isQuoteToken ? getQuoteDecimals() : 18;
104
+ const marketDetailRes = await client.markets.getMarketDetail({ chainId, poolId: args.poolId });
105
+ const marketDetail = marketDetailRes?.data || (marketDetailRes?.marketId ? marketDetailRes : null);
106
+ if (!marketDetail?.marketId) {
107
+ throw new Error(`Could not resolve market metadata for poolId=${args.poolId}.`);
108
+ }
109
+ const decimals = Number(Boolean(args.isQuoteToken)
110
+ ? marketDetail.quoteDecimals
111
+ : marketDetail.baseDecimals);
112
+ if (!Number.isFinite(decimals) || decimals < 0) {
113
+ throw new Error(`Invalid token decimals for withdraw on poolId=${args.poolId}.`);
114
+ }
108
115
  const amount = ensureUnits(args.amount, decimals, "amount", { allowImplicitRaw: false });
109
116
  const amountRaw = asBigintOrNull(amount);
110
117
  if (amountRaw === null || amountRaw <= 0n) {
@@ -1,9 +1,25 @@
1
1
  import { z } from "zod";
2
2
  import { resolveClient, getChainId, getQuoteToken } from "../auth/resolveClient.js";
3
- import { getOraclePrice, resolvePool } from "../services/marketService.js";
3
+ import { getFreshOraclePrice, resolvePool } from "../services/marketService.js";
4
4
  import { ensureUnits } from "../utils/units.js";
5
5
  import { finalizeMutationResult } from "../utils/mutationResult.js";
6
6
  import { normalizeSlippagePct4dpFlexible, SLIPPAGE_PCT_4DP_DESC } from "../utils/slippage.js";
7
+ const INTEGER_RE = /^\d+$/;
8
+ function resolvePositionSizeInput(position) {
9
+ const rawCandidates = [position?.sizeRaw, position?.positionSizeRaw];
10
+ for (const candidate of rawCandidates) {
11
+ const text = String(candidate ?? "").trim();
12
+ if (INTEGER_RE.test(text))
13
+ return `raw:${text}`;
14
+ }
15
+ const humanCandidates = [position?.size, position?.positionSize];
16
+ for (const candidate of humanCandidates) {
17
+ const text = String(candidate ?? "").trim();
18
+ if (text)
19
+ return text;
20
+ }
21
+ throw new Error(`Position size missing for positionId=${String(position?.positionId ?? position?.position_id ?? "").trim()}.`);
22
+ }
7
23
  export const closeAllPositionsTool = {
8
24
  name: "close_all_positions",
9
25
  description: "[TRADE] Emergency: close ALL open positions in a pool at once. Use for risk management.",
@@ -28,27 +44,18 @@ export const closeAllPositionsTool = {
28
44
  return { content: [{ type: "text", text: JSON.stringify({ status: "success", data: { message: "No open positions in this pool.", closed: 0 } }, (_, v) => typeof v === 'bigint' ? v.toString() : v, 2) }] };
29
45
  }
30
46
  // 2) 为每个仓位构建平仓参数
31
- const slippagePct = normalizeSlippagePct4dpFlexible(args.slippagePct ?? "200");
32
- // 3) We need actual oracle prices to avoid Revert 0x613970e0 (InvalidParameter)
33
- const oraclePriceReq = await getOraclePrice(client, poolId).catch(() => null);
34
- let fallbackPrice = "0";
35
- if (oraclePriceReq && oraclePriceReq.price) {
36
- fallbackPrice = oraclePriceReq.price.toString();
37
- }
38
- else {
39
- const { getMarketPrice } = await import("../services/marketService.js");
40
- const marketData = await getMarketPrice(client, poolId).catch(() => null);
41
- if (marketData && marketData.price) {
42
- fallbackPrice = marketData.price.toString();
43
- }
44
- else {
45
- // Extreme fallback - but this may still revert
46
- fallbackPrice = poolPositions[0]?.entryPrice || "0";
47
- }
47
+ const slippagePct = normalizeSlippagePct4dpFlexible(args.slippagePct ?? "100");
48
+ const marketDetailRes = await client.markets.getMarketDetail({ chainId, poolId });
49
+ const marketDetail = marketDetailRes?.data || (marketDetailRes?.marketId ? marketDetailRes : null);
50
+ if (!marketDetail?.marketId) {
51
+ throw new Error(`Could not resolve market metadata for poolId=${poolId}.`);
48
52
  }
53
+ const baseDecimals = Number(marketDetail.baseDecimals ?? 18);
54
+ const quoteToken = String(marketDetail.quoteToken ?? "").trim() || getQuoteToken();
55
+ const oracle = await getFreshOraclePrice(client, poolId, chainId);
56
+ const freshOraclePrice = oracle.price.toString();
49
57
  const closeParams = poolPositions.map((pos) => {
50
- const rawSize = pos.size || pos.positionSize || "0";
51
- const rawPrice = pos.markPrice || pos.mark_price || fallbackPrice;
58
+ const sizeInput = resolvePositionSizeInput(pos);
52
59
  return {
53
60
  chainId,
54
61
  address,
@@ -59,11 +66,11 @@ export const closeAllPositionsTool = {
59
66
  timeInForce: 0, // IOC
60
67
  direction: pos.direction ?? 0,
61
68
  collateralAmount: "0",
62
- size: ensureUnits(rawSize, 18, "size"),
63
- price: ensureUnits(rawPrice, 30, "price"),
69
+ size: ensureUnits(sizeInput, baseDecimals, "size", { allowImplicitRaw: false }),
70
+ price: ensureUnits(freshOraclePrice, 30, "price", { allowImplicitRaw: false }),
64
71
  postOnly: false,
65
72
  slippagePct,
66
- executionFeeToken: pos.quoteToken || pos.quote_token || getQuoteToken(),
73
+ executionFeeToken: quoteToken,
67
74
  leverage: pos.userLeverage ?? pos.leverage ?? 1,
68
75
  };
69
76
  });
@@ -41,7 +41,7 @@ export const closePositionTool = {
41
41
  collateralAmount: z.union([z.string(), z.number()]).describe("Collateral amount (human/raw). Also supports ALL/FULL/MAX to use live position collateral raw."),
42
42
  size: z.union([z.string(), z.number()]).describe("Position size (human/raw). Also supports ALL/FULL/MAX for exact full-close raw size."),
43
43
  price: z.union([z.string(), z.number()]).describe("Price (human or 30-dec raw units)"),
44
- timeInForce: z.union([z.number(), z.string()]).describe("SDK v1.0.2 supports IOC only. Use 0 or 'IOC'."),
44
+ timeInForce: z.union([z.number(), z.string()]).describe("SDK v1.0.4-beta.4 supports IOC only. Use 0 or 'IOC'."),
45
45
  postOnly: z.coerce.boolean().describe("Post-only flag"),
46
46
  slippagePct: z.coerce.string().default("50").describe(SLIPPAGE_PCT_4DP_DESC),
47
47
  executionFeeToken: z.string().describe("Execution fee token address"),
@@ -95,6 +95,20 @@ export const closePositionTool = {
95
95
  triggerType: preparedArgs.triggerType !== undefined ? mapTriggerType(preparedArgs.triggerType) : undefined,
96
96
  executionFeeToken: poolData.quoteToken || preparedArgs.executionFeeToken
97
97
  };
98
+ const positionsRes = await client.position.listPositions(address);
99
+ const positions = Array.isArray(positionsRes?.data) ? positionsRes.data : [];
100
+ const target = positions.find((position) => {
101
+ const pid = String(position?.positionId ?? position?.position_id ?? "").trim().toLowerCase();
102
+ const pool = String(position?.poolId ?? position?.pool_id ?? "").trim().toLowerCase();
103
+ return pid === String(preparedArgs.positionId ?? "").trim().toLowerCase() && pool === String(poolId).toLowerCase();
104
+ });
105
+ if (!target) {
106
+ throw new Error(`Could not find live position snapshot for positionId=${preparedArgs.positionId} in poolId=${poolId}.`);
107
+ }
108
+ const liveDirection = Number(target?.direction);
109
+ if (!Number.isFinite(liveDirection) || liveDirection !== mappedArgs.direction) {
110
+ throw new Error(`direction mismatch for positionId=${preparedArgs.positionId}: input=${mappedArgs.direction}, live=${String(target?.direction ?? "unknown")}.`);
111
+ }
98
112
  const raw = await closePos(client, address, mappedArgs);
99
113
  const data = await finalizeMutationResult(raw, signer, "close_position");
100
114
  const txHash = data.confirmation?.txHash;
@@ -10,6 +10,14 @@ import { parseUserUnits } from "../utils/units.js";
10
10
  import { isZeroAddress } from "../utils/address.js";
11
11
  const POSITION_ID_RE = /^$|^0x[0-9a-fA-F]{64}$/;
12
12
  const ZERO_POSITION_ID_RE = /^0x0{64}$/i;
13
+ function pow10(decimals) {
14
+ return 10n ** BigInt(decimals);
15
+ }
16
+ function computeQuoteNotionalRaw(sizeRaw, priceRaw30, baseDecimals, quoteDecimals) {
17
+ const numerator = sizeRaw * priceRaw30 * pow10(quoteDecimals);
18
+ const denominator = pow10(baseDecimals + 30);
19
+ return numerator / denominator;
20
+ }
13
21
  export const executeTradeTool = {
14
22
  name: "execute_trade",
15
23
  description: "[TRADE] Create an increase order (open or add to position) using SDK-native parameters.",
@@ -24,7 +32,7 @@ export const executeTradeTool = {
24
32
  collateralAmount: z.union([z.string(), z.number()]).describe("Collateral. e.g. '100' or 'raw:100000000' (6 decimals for USDC)."),
25
33
  size: z.union([z.string(), z.number()]).describe("Notional size in base tokens. e.g. '0.5' BTC or 'raw:50000000'."),
26
34
  price: z.union([z.string(), z.number()]).describe("Execution or Limit price. e.g. '65000' or 'raw:...'"),
27
- timeInForce: z.union([z.number(), z.string()]).describe("SDK v1.0.2 supports IOC only. Use 0 or 'IOC'."),
35
+ timeInForce: z.union([z.number(), z.string()]).describe("SDK v1.0.4-beta.4 supports IOC only. Use 0 or 'IOC'."),
28
36
  postOnly: z.coerce.boolean().describe("If true, order only executes as Maker."),
29
37
  slippagePct: z.coerce.string().default("50").describe(`${SLIPPAGE_PCT_4DP_DESC}. Default is 50 (0.5%).`),
30
38
  executionFeeToken: z.string().optional().describe("Address of token to pay gas/execution fees (typically USDC). Default is pool quoteToken."),
@@ -76,6 +84,22 @@ export const executeTradeTool = {
76
84
  throw new Error("size must be > 0.");
77
85
  if (BigInt(priceRaw) <= 0n)
78
86
  throw new Error("price must be > 0.");
87
+ if (normalizedPositionId) {
88
+ const positionsRes = await client.position.listPositions(address);
89
+ const positions = Array.isArray(positionsRes?.data) ? positionsRes.data : [];
90
+ const target = positions.find((position) => {
91
+ const positionId = String(position?.positionId ?? position?.position_id ?? "").trim().toLowerCase();
92
+ const positionPoolId = String(position?.poolId ?? position?.pool_id ?? "").trim().toLowerCase();
93
+ return positionId === normalizedPositionId.toLowerCase() && positionPoolId === String(poolId).toLowerCase();
94
+ });
95
+ if (!target) {
96
+ throw new Error(`Could not find live position for positionId=${normalizedPositionId} in poolId=${poolId}.`);
97
+ }
98
+ const liveDirection = Number(target?.direction);
99
+ if (!Number.isFinite(liveDirection) || liveDirection !== mappedDirection) {
100
+ throw new Error(`direction mismatch for positionId=${normalizedPositionId}: input=${mappedDirection}, live=${String(target?.direction ?? "unknown")}.`);
101
+ }
102
+ }
79
103
  let tradingFeeRaw = "";
80
104
  let tradingFeeMeta = { source: "user" };
81
105
  const tradingFeeInput = String(args.tradingFee ?? "").trim();
@@ -107,7 +131,8 @@ export const executeTradeTool = {
107
131
  }
108
132
  const rateRaw = args.postOnly ? feeRes.data.makerFeeRate : feeRes.data.takerFeeRate;
109
133
  const rateBig = BigInt(String(rateRaw ?? "0"));
110
- tradingFeeRaw = ((BigInt(collateralRaw) * rateBig) / 1000000n).toString();
134
+ const notionalQuoteRaw = computeQuoteNotionalRaw(BigInt(sizeRaw), BigInt(priceRaw), baseDecimals, quoteDecimals);
135
+ tradingFeeRaw = ((notionalQuoteRaw * rateBig) / 1000000n).toString();
111
136
  tradingFeeMeta = { source: "computed", assetClass, riskTier, feeRate: String(rateRaw ?? "0") };
112
137
  }
113
138
  const mappedArgs = {
@@ -121,9 +121,6 @@ function toSymbol(row) {
121
121
  return base;
122
122
  return normalizePoolId(row?.poolId ?? row?.pool_id);
123
123
  }
124
- function sumRaw(baseRaw, quoteRaw) {
125
- return BigInt(baseRaw || "0") + BigInt(quoteRaw || "0");
126
- }
127
124
  async function readErc20Balance(provider, tokenAddress, holder) {
128
125
  const contract = new Contract(tokenAddress, ERC20_BALANCE_ABI, provider);
129
126
  const balance = await contract.balanceOf(holder);
@@ -227,11 +224,10 @@ export const getMyLpHoldingsTool = {
227
224
  });
228
225
  }
229
226
  items.sort((left, right) => {
230
- const leftSum = sumRaw(left.baseLpBalanceRaw, left.quoteLpBalanceRaw);
231
- const rightSum = sumRaw(right.baseLpBalanceRaw, right.quoteLpBalanceRaw);
232
- if (leftSum === rightSum)
233
- return 0;
234
- return rightSum > leftSum ? 1 : -1;
227
+ const symbolCompare = String(left.symbol ?? "").localeCompare(String(right.symbol ?? ""));
228
+ if (symbolCompare !== 0)
229
+ return symbolCompare;
230
+ return String(left.poolId ?? "").localeCompare(String(right.poolId ?? ""));
235
231
  });
236
232
  const payload = {
237
233
  meta: {
@@ -4,21 +4,8 @@ import { resolveClient, getChainId } from "../auth/resolveClient.js";
4
4
  import { getMarketDetail, resolvePool } from "../services/marketService.js";
5
5
  import { getPoolInfo, getLiquidityInfo } from "../services/poolService.js";
6
6
  import { extractErrorMessage } from "../utils/errorMessage.js";
7
- import { parseUserPrice30 } from "../utils/units.js";
8
7
  const INTEGER_RE = /^\d+$/;
9
8
  const SIGNED_INTEGER_RE = /^-?\d+$/;
10
- const DECIMAL_RE = /^\d+(\.\d+)?$/;
11
- function normalizeMarketPrice30Input(value) {
12
- const text = String(value ?? "").trim();
13
- if (!text)
14
- return "";
15
- if (INTEGER_RE.test(text))
16
- return text;
17
- if (!DECIMAL_RE.test(text) && !/^raw:/i.test(text) && !/^human:/i.test(text)) {
18
- throw new Error("marketPrice must be numeric, or prefixed with raw:/human:.");
19
- }
20
- return parseUserPrice30(text, "marketPrice");
21
- }
22
9
  function compactWarning(scope, err) {
23
10
  const raw = extractErrorMessage(err);
24
11
  const flat = raw.replace(/\s+/g, " ").trim();
@@ -211,8 +198,8 @@ export const getPoolMetadataTool = {
211
198
  schema: {
212
199
  poolId: z.string().optional().describe("Pool ID, Token Address, or Keyword"),
213
200
  keyword: z.string().optional().describe("Market keyword (e.g. 'BTC')"),
214
- includeLiquidity: z.boolean().default(false).describe("Whether to include liquidity depth (requires marketPrice)"),
215
- marketPrice: z.union([z.string(), z.number()]).optional().describe("Current price (required if includeLiquidity is true)"),
201
+ includeLiquidity: z.boolean().default(false).describe("Whether to include liquidity depth (uses fresh oracle price automatically)"),
202
+ marketPrice: z.union([z.string(), z.number()]).optional().describe("Deprecated and ignored. MCP now uses fresh oracle price for liquidity depth."),
216
203
  includeConfig: z.boolean().default(false).describe("Whether to include pool level configuration/limits"),
217
204
  chainId: z.number().int().positive().optional().describe("Optional chainId override"),
218
205
  },
@@ -245,10 +232,15 @@ export const getPoolMetadataTool = {
245
232
  // 3. Optional: Liquidity Info
246
233
  if (args.includeLiquidity) {
247
234
  try {
248
- const price = normalizeMarketPrice30Input(args.marketPrice);
249
- if (!price)
250
- throw new Error("marketPrice is required for liquidity info.");
251
- results.liquidityInfo = await getLiquidityInfo(client, poolId, price, chainId);
235
+ const liquidityResult = await getLiquidityInfo(client, poolId, chainId);
236
+ results.liquidityInfo = liquidityResult.liquidityInfo;
237
+ results.liquidityInfoMeta = {
238
+ marketPriceSource: liquidityResult.marketPriceSource,
239
+ marketPrice: liquidityResult.marketPrice,
240
+ oraclePublishTime: liquidityResult.oraclePublishTime,
241
+ oracleType: liquidityResult.oracleType,
242
+ ignoredUserMarketPrice: args.marketPrice !== undefined ? String(args.marketPrice) : null,
243
+ };
252
244
  }
253
245
  catch (err) {
254
246
  errors.push(compactWarning("liquidityInfo", err));
@@ -5,6 +5,7 @@ import { resolveClient, getChainId } from "../auth/resolveClient.js";
5
5
  import { resolvePool } from "../services/marketService.js";
6
6
  import { finalizeMutationResult } from "../utils/mutationResult.js";
7
7
  import { extractErrorMessage } from "../utils/errorMessage.js";
8
+ import { normalizeLpSlippageRatio } from "../utils/slippage.js";
8
9
  function normalizeAssetSymbol(value) {
9
10
  const text = String(value ?? "").trim();
10
11
  if (!text)
@@ -67,7 +68,7 @@ export const manageLiquidityTool = {
67
68
  action: z.coerce.string().describe("'deposit' or 'withdraw' (aliases: add/remove/increase/decrease; case-insensitive)"),
68
69
  poolType: z.enum(["BASE", "QUOTE"]).describe("'BASE' or 'QUOTE'"),
69
70
  poolId: z.string().describe("Pool ID or Base Token Address"),
70
- amount: z.coerce.number().positive().describe("Amount in human-readable units"),
71
+ amount: z.coerce.string().describe("Amount in human-readable units string"),
71
72
  slippage: z.coerce.number().min(0).describe("LP slippage ratio (e.g. 0.01 = 1%)"),
72
73
  chainId: z.coerce.number().int().positive().optional().describe("Optional chainId override"),
73
74
  },
@@ -75,9 +76,13 @@ export const manageLiquidityTool = {
75
76
  try {
76
77
  const { client, signer } = await resolveClient();
77
78
  let { action, poolType, poolId } = args;
78
- const { amount, slippage } = args;
79
+ const amount = String(args.amount ?? "").trim();
80
+ const slippage = normalizeLpSlippageRatio(args.slippage);
79
81
  const chainId = args.chainId ?? getChainId();
80
82
  action = String(action ?? "").trim().toLowerCase();
83
+ if (!amount) {
84
+ throw new Error("amount is required.");
85
+ }
81
86
  const validActions = new Set(["deposit", "withdraw", "add", "remove", "increase", "decrease"]);
82
87
  if (!validActions.has(action)) {
83
88
  throw new Error(`Invalid action: ${args.action}. Use deposit/withdraw or aliases add/remove/increase/decrease.`);
@@ -29,7 +29,7 @@ function readSnapshotText(snapshot, keys) {
29
29
  }
30
30
  return "";
31
31
  }
32
- function normalizeUnitsInput(text) {
32
+ function normalizeSizeInput(text) {
33
33
  const raw = String(text ?? "").trim();
34
34
  if (!raw)
35
35
  return "";
@@ -39,6 +39,17 @@ function normalizeUnitsInput(text) {
39
39
  return `raw:${raw}`;
40
40
  return raw;
41
41
  }
42
+ function normalizePriceInput(text, source = "user") {
43
+ const raw = String(text ?? "").trim();
44
+ if (!raw)
45
+ return "";
46
+ if (/^(raw|human):/i.test(raw))
47
+ return raw;
48
+ if (source === "snapshot" && /^\d+$/.test(raw) && raw.length > 12) {
49
+ return `raw:${raw}`;
50
+ }
51
+ return raw;
52
+ }
42
53
  function isExplicitZeroValue(value) {
43
54
  if (value === undefined || value === null)
44
55
  return false;
@@ -160,6 +171,58 @@ function resolvePositionSizeRaw(positionSnapshot, baseDecimals) {
160
171
  }
161
172
  return "";
162
173
  }
174
+ function resolveEntryPriceInput(positionSnapshot) {
175
+ const rawCandidates = [
176
+ positionSnapshot?.entryPriceRaw,
177
+ positionSnapshot?.openPriceRaw,
178
+ positionSnapshot?.avgPriceRaw,
179
+ positionSnapshot?.averageOpenPriceRaw,
180
+ ];
181
+ for (const value of rawCandidates) {
182
+ const text = String(value ?? "").trim();
183
+ if (INTEGER_RE.test(text))
184
+ return `raw:${text}`;
185
+ }
186
+ const humanCandidates = [
187
+ positionSnapshot?.entryPrice,
188
+ positionSnapshot?.openPrice,
189
+ positionSnapshot?.avgPrice,
190
+ positionSnapshot?.averageOpenPrice,
191
+ ];
192
+ for (const value of humanCandidates) {
193
+ const text = String(value ?? "").trim();
194
+ if (text)
195
+ return text;
196
+ }
197
+ return "";
198
+ }
199
+ function validateTpSlPriceSemantics(direction, entryPriceInput, tpPriceInput, slPriceInput) {
200
+ if (!entryPriceInput) {
201
+ throw new Error("Unable to resolve entryPrice for TP/SL validation.");
202
+ }
203
+ const entryPriceRaw = BigInt(parseUserUnits(entryPriceInput, 30, "entryPrice"));
204
+ if (entryPriceRaw <= 0n) {
205
+ throw new Error("entryPrice must be > 0 for TP/SL validation.");
206
+ }
207
+ if (tpPriceInput) {
208
+ const tpPriceRaw = BigInt(parseUserUnits(tpPriceInput, 30, "tpPrice"));
209
+ if (direction === 0 && tpPriceRaw <= entryPriceRaw) {
210
+ throw new Error("LONG TP must be greater than entryPrice.");
211
+ }
212
+ if (direction === 1 && tpPriceRaw >= entryPriceRaw) {
213
+ throw new Error("SHORT TP must be less than entryPrice.");
214
+ }
215
+ }
216
+ if (slPriceInput) {
217
+ const slPriceRaw = BigInt(parseUserUnits(slPriceInput, 30, "slPrice"));
218
+ if (direction === 0 && slPriceRaw >= entryPriceRaw) {
219
+ throw new Error("LONG SL must be less than entryPrice.");
220
+ }
221
+ if (direction === 1 && slPriceRaw <= entryPriceRaw) {
222
+ throw new Error("SHORT SL must be greater than entryPrice.");
223
+ }
224
+ }
225
+ }
163
226
  async function cancelTpSlByIntent(client, address, signer, chainId, args) {
164
227
  if (args.orderId) {
165
228
  const raw = await client.order.cancelAllOrders([String(args.orderId)], chainId);
@@ -265,12 +328,21 @@ export const manageTpSlTool = {
265
328
  const snapshotTpSize = readSnapshotText(orderSnapshot, ["tpSize", "takeProfitSize"]);
266
329
  const snapshotSlPrice = readSnapshotText(orderSnapshot, ["slPrice", "stopLossPrice", "slTriggerPrice"]);
267
330
  const snapshotSlSize = readSnapshotText(orderSnapshot, ["slSize", "stopLossSize"]);
331
+ const orderPositionId = readSnapshotText(orderSnapshot, ["positionId", "position_id"]);
268
332
  const size = isNonEmpty(args.size) ? String(args.size) : snapshotSize;
269
333
  const price = isNonEmpty(args.price) ? String(args.price) : snapshotPrice;
270
334
  const tpPrice = isNonEmpty(args.tpPrice) ? String(args.tpPrice) : snapshotTpPrice;
271
335
  const tpSize = isNonEmpty(args.tpSize) ? String(args.tpSize) : snapshotTpSize;
272
336
  const slPrice = isNonEmpty(args.slPrice) ? String(args.slPrice) : snapshotSlPrice;
273
337
  const slSize = isNonEmpty(args.slSize) ? String(args.slSize) : snapshotSlSize;
338
+ const updateDirection = normalizeDirectionInput(args.direction ?? orderSnapshot?.direction);
339
+ const positionSnapshot = orderPositionId
340
+ ? await findPositionSnapshot(client, address, args.poolId, orderPositionId)
341
+ : null;
342
+ if (updateDirection !== undefined) {
343
+ const entryPriceInput = resolveEntryPriceInput(positionSnapshot);
344
+ validateTpSlPriceSemantics(updateDirection, entryPriceInput, tpPrice || undefined, slPrice || undefined);
345
+ }
274
346
  if (!size || !price) {
275
347
  throw new Error("size and price are required for update. Provide them explicitly, or ensure orderId can be found via get_orders so they can be auto-resolved.");
276
348
  }
@@ -288,12 +360,12 @@ export const manageTpSlTool = {
288
360
  orderId: args.orderId,
289
361
  marketId: market.marketId,
290
362
  poolId: args.poolId,
291
- size: normalizeUnitsInput(size),
292
- price: normalizeUnitsInput(price),
293
- tpPrice: tpPrice ? normalizeUnitsInput(tpPrice) : "0",
294
- tpSize: tpSize ? normalizeUnitsInput(tpSize) : "0",
295
- slPrice: slPrice ? normalizeUnitsInput(slPrice) : "0",
296
- slSize: slSize ? normalizeUnitsInput(slSize) : "0",
363
+ size: normalizeSizeInput(size),
364
+ price: normalizePriceInput(price, isNonEmpty(args.price) ? "user" : "snapshot"),
365
+ tpPrice: tpPrice ? normalizePriceInput(tpPrice, isNonEmpty(args.tpPrice) ? "user" : "snapshot") : "0",
366
+ tpSize: tpSize ? normalizeSizeInput(tpSize) : "0",
367
+ slPrice: slPrice ? normalizePriceInput(slPrice, isNonEmpty(args.slPrice) ? "user" : "snapshot") : "0",
368
+ slSize: slSize ? normalizeSizeInput(slSize) : "0",
297
369
  quoteToken: market.quoteToken,
298
370
  useOrderCollateral: args.useOrderCollateral ?? true
299
371
  }, chainId);
@@ -326,6 +398,8 @@ export const manageTpSlTool = {
326
398
  if (resolvedDirection === undefined || !resolvedLeverage || Number(resolvedLeverage) <= 0) {
327
399
  throw new Error("direction and leverage are required when creating new TP/SL.");
328
400
  }
401
+ const entryPriceInput = resolveEntryPriceInput(positionSnapshot);
402
+ validateTpSlPriceSemantics(resolvedDirection, entryPriceInput, isNonEmpty(args.tpPrice) ? String(args.tpPrice) : undefined, isNonEmpty(args.slPrice) ? String(args.slPrice) : undefined);
329
403
  let tpSizeInput = isNonEmpty(args.tpSize) ? String(args.tpSize) : "";
330
404
  let slSizeInput = isNonEmpty(args.slSize) ? String(args.slSize) : "";
331
405
  const needsTpSize = isNonEmpty(args.tpPrice) && !tpSizeInput;