@michaleffffff/mcp-trading-server 2.9.2 → 3.0.0

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.
@@ -12,24 +12,27 @@ export const tradingGuidePrompt = {
12
12
  content: {
13
13
  type: "text",
14
14
  text: `
15
- # MYX Trading MCP Best Practices (v2.8.0)
15
+ # MYX Trading MCP Best Practices (v3.0.0)
16
16
 
17
17
  You are an expert crypto trader using the MYX Protocol. To ensure successful execution and safe handling of user funds, follow these patterns:
18
18
 
19
19
  ## 1. The Standard Workflow
20
- 1. **Discovery**: Use \`search_market\` with a keyword (e.g., "BTC") to find the active \`poolId\`.
21
- 2. **Context**: Use \`get_market_price\` and \`get_account_info\` to check the current market state and your available margin.
20
+ 1. **Discovery**: Use \`search_market\` with a keyword (or empty keyword) to find active \`poolId\` values. If needed, fallback to \`get_market_list\`.
21
+ 2. **Context**: Use \`get_market_price\` and \`get_account\` (with \`poolId\`) to check market state, wallet balance, and trading-account margin.
22
22
  3. **Execution**: Prefer \`open_position_simple\` for new trades. It handles unit conversions and pool resolution automatically.
23
23
  4. **Validation**: Always check the \`verification.verified\` flag in the output. If \`false\`, read the \`cancelReason\` to explain the failure to the user.
24
+ 5. **Limit Order Loop**: For limit orders, always follow \`execute_trade -> get_open_orders -> get_order_history -> cancel_order/cancel_all_orders\`.
24
25
 
25
26
  ## 2. Parameter Tips
26
27
  - **Position IDs**: When opening a NEW position, \`positionId\` MUST be an empty string \`""\`.
27
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.
28
29
  - **Slippage**: Default is 100 (1%). For volatile meme tokens, consider 200-300 (2-3%).
29
30
  - **Fees**: Use \`get_user_trading_fee_rate\` to estimate fees before large trades.
31
+ - **Balances**: Use \`get_account\` to clearly separate wallet balance vs trading-account balance (pass \`poolId\` for trading-account metrics).
32
+ - **Examples**: Follow \`TOOL_EXAMPLES.md\` payload patterns when building tool arguments.
30
33
 
31
34
  ## 3. Self-Healing
32
- If a transaction reverts with a hex code, the server will attempt to decode it (e.g., "AccountInsufficientFreeAmount"). Inform the user specifically about what is missing rather than giving a generic error.
35
+ If a transaction reverts with a hex code, the server will attempt to decode it (e.g., "AccountInsufficientFreeAmount"). Error payloads now include structured \`code/hint/action\` fields; use them to provide concrete next steps.
33
36
 
34
37
  Current Session:
35
38
  - Wallet: ${address}
package/dist/server.js CHANGED
@@ -4,7 +4,7 @@ import "dotenv/config";
4
4
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
5
5
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
6
  import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
7
- import { z } from "zod";
7
+ import { ZodError, z } from "zod";
8
8
  // Tools & Modules
9
9
  import * as baseTools from "./tools/index.js";
10
10
  import * as baseResources from "./resources/index.js";
@@ -33,6 +33,186 @@ process.on('SIGTERM', shutdown);
33
33
  const allTools = Object.values(baseTools);
34
34
  const allResources = Object.values(baseResources);
35
35
  const allPrompts = Object.values(basePrompts);
36
+ function safeJsonStringify(value) {
37
+ return JSON.stringify(value, (_, item) => (typeof item === "bigint" ? item.toString() : item), 2);
38
+ }
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
+ function inferToolErrorCode(message) {
50
+ const lower = message.toLowerCase();
51
+ if (lower.includes("required") || lower.includes("invalid") || lower.includes("must be") || lower.includes("unexpected") || lower.includes("unrecognized")) {
52
+ return "INVALID_PARAM";
53
+ }
54
+ if (lower.includes("insufficient") && (lower.includes("allowance") || lower.includes("approval"))) {
55
+ return "INSUFFICIENT_ALLOWANCE";
56
+ }
57
+ if (lower.includes("insufficient") || lower.includes("not enough balance") || lower.includes("insufficientbalance")) {
58
+ return "INSUFFICIENT_BALANCE";
59
+ }
60
+ if (lower.includes("not found") || lower.includes("unknown tool")) {
61
+ return "NOT_FOUND";
62
+ }
63
+ if (lower.includes("timeout") || lower.includes("not confirmed")) {
64
+ return "TIMEOUT";
65
+ }
66
+ if (lower.includes("network") || lower.includes("rpc")) {
67
+ return "NETWORK_ERROR";
68
+ }
69
+ return "TOOL_EXECUTION_ERROR";
70
+ }
71
+ function defaultHintForErrorCode(code, toolName) {
72
+ if (code === "INVALID_PARAM") {
73
+ return `Check required fields/types for "${toolName}" and retry.`;
74
+ }
75
+ if (code === "INSUFFICIENT_ALLOWANCE") {
76
+ return `Run "check_approval" before retrying "${toolName}".`;
77
+ }
78
+ if (code === "INSUFFICIENT_BALANCE") {
79
+ return "Top up wallet/trading-account balance or reduce order size.";
80
+ }
81
+ if (code === "NOT_FOUND") {
82
+ return "Verify identifiers (poolId/orderId/marketId) and retry.";
83
+ }
84
+ if (code === "TIMEOUT") {
85
+ return "Query order/position status first, then retry only if needed.";
86
+ }
87
+ if (code === "NETWORK_ERROR") {
88
+ return "Check RPC/network health and retry.";
89
+ }
90
+ return `Check prerequisites for "${toolName}" and retry.`;
91
+ }
92
+ function errorResult(payload) {
93
+ const body = {
94
+ status: "error",
95
+ error: {
96
+ tool: payload.tool,
97
+ code: payload.code,
98
+ message: payload.message,
99
+ hint: payload.hint,
100
+ action: payload.action,
101
+ details: payload.details,
102
+ },
103
+ };
104
+ return {
105
+ content: [{ type: "text", text: safeJsonStringify(body) }],
106
+ isError: true,
107
+ };
108
+ }
109
+ function parseFirstText(result) {
110
+ if (!result || !Array.isArray(result.content))
111
+ return null;
112
+ const firstText = result.content.find((item) => item?.type === "text" && typeof item.text === "string");
113
+ return firstText?.text ?? null;
114
+ }
115
+ function normalizeToolErrorResult(toolName, result) {
116
+ const rawText = parseFirstText(result);
117
+ if (!rawText) {
118
+ return errorResult({
119
+ tool: toolName,
120
+ code: "TOOL_EXECUTION_ERROR",
121
+ message: `Tool "${toolName}" returned an empty error.`,
122
+ hint: defaultHintForErrorCode("TOOL_EXECUTION_ERROR", toolName),
123
+ action: "Inspect server logs for details.",
124
+ });
125
+ }
126
+ let parsed = null;
127
+ try {
128
+ parsed = JSON.parse(rawText);
129
+ }
130
+ catch {
131
+ parsed = null;
132
+ }
133
+ if (parsed &&
134
+ parsed.status === "error" &&
135
+ parsed.error &&
136
+ typeof parsed.error.code === "string" &&
137
+ typeof parsed.error.message === "string") {
138
+ return result;
139
+ }
140
+ const plainMessage = typeof parsed?.error?.message === "string"
141
+ ? parsed.error.message
142
+ : typeof parsed?.message === "string"
143
+ ? parsed.message
144
+ : rawText.replace(/^Error:\s*/i, "").trim();
145
+ const code = typeof parsed?.error?.code === "string"
146
+ ? parsed.error.code
147
+ : inferToolErrorCode(plainMessage);
148
+ return errorResult({
149
+ tool: toolName,
150
+ code,
151
+ message: plainMessage,
152
+ hint: defaultHintForErrorCode(code, toolName),
153
+ action: "Adjust parameters/prerequisites and retry.",
154
+ });
155
+ }
156
+ function validationErrorResult(toolName, error) {
157
+ const issues = error.issues.map((issue) => {
158
+ const pathValue = issue.path.length > 0 ? issue.path.map(String).join(".") : "input";
159
+ return {
160
+ field: pathValue,
161
+ code: issue.code,
162
+ message: issue.message,
163
+ };
164
+ });
165
+ const firstIssue = issues[0];
166
+ const firstHint = firstIssue
167
+ ? `Fix "${firstIssue.field}": ${firstIssue.message}`
168
+ : `Check input schema for "${toolName}".`;
169
+ return errorResult({
170
+ tool: toolName,
171
+ code: "INVALID_PARAM",
172
+ message: `Invalid arguments for tool "${toolName}".`,
173
+ hint: firstHint,
174
+ action: "Call list_tools and resend valid arguments.",
175
+ details: { issues },
176
+ });
177
+ }
178
+ function normalizeSdkReadFailure(toolName, result) {
179
+ const rawText = parseFirstText(result);
180
+ if (!rawText)
181
+ return result;
182
+ let parsed = null;
183
+ try {
184
+ parsed = JSON.parse(rawText);
185
+ }
186
+ catch {
187
+ return result;
188
+ }
189
+ if (!parsed || parsed.status !== "success")
190
+ return result;
191
+ const data = parsed.data;
192
+ const hasCode = !!data && typeof data === "object" && !Array.isArray(data) && Object.prototype.hasOwnProperty.call(data, "code");
193
+ if (!hasCode)
194
+ return result;
195
+ const code = Number(data.code);
196
+ if (!Number.isFinite(code) || code === 0)
197
+ return result;
198
+ const sdkPayload = data.data;
199
+ const hasPayload = sdkPayload !== null &&
200
+ sdkPayload !== undefined &&
201
+ (!Array.isArray(sdkPayload) || sdkPayload.length > 0);
202
+ if (code > 0 && hasPayload) {
203
+ // Some SDK APIs return non-zero positive codes with usable payloads.
204
+ return result;
205
+ }
206
+ const sdkMessage = String(data.msg ?? data.message ?? `SDK read failed with code=${code}.`);
207
+ return errorResult({
208
+ tool: toolName,
209
+ code: "SDK_READ_ERROR",
210
+ message: `${toolName} failed: code=${code}, msg=${sdkMessage}`,
211
+ hint: "Check chain/account context and required params, then retry.",
212
+ action: "Retry or call prerequisite tools to refresh context.",
213
+ details: { sdk: data },
214
+ });
215
+ }
36
216
  // 将 Zod schema 转换为 JSON Schema (tool listing 用)
37
217
  function zodSchemaToJsonSchema(zodSchema) {
38
218
  const toPropSchema = (value) => {
@@ -81,7 +261,7 @@ function zodSchemaToJsonSchema(zodSchema) {
81
261
  };
82
262
  }
83
263
  // ─── MCP Server ───
84
- const server = new Server({ name: "myx-mcp-trading-server", version: "2.9.2" }, { capabilities: { tools: {}, resources: {}, prompts: {} } });
264
+ const server = new Server({ name: "myx-mcp-trading-server", version: "3.0.0" }, { capabilities: { tools: {}, resources: {}, prompts: {} } });
85
265
  // List tools
86
266
  server.setRequestHandler(ListToolsRequestSchema, async () => {
87
267
  return {
@@ -97,30 +277,50 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
97
277
  const { name, arguments: args } = request.params;
98
278
  const tool = allTools.find((t) => t.name === name);
99
279
  if (!tool) {
100
- return {
101
- content: [{ type: "text", text: `Error: Unknown tool "${name}"` }],
102
- isError: true,
103
- };
280
+ return errorResult({
281
+ tool: name,
282
+ code: "NOT_FOUND",
283
+ message: `Unknown tool "${name}".`,
284
+ hint: "Use list_tools to discover available tool names.",
285
+ action: "Retry with a valid tool name.",
286
+ });
104
287
  }
105
288
  try {
106
289
  let validatedArgs = args ?? {};
107
290
  if (tool.schema) {
108
- validatedArgs = z.object(tool.schema).strict().parse(args ?? {});
291
+ try {
292
+ validatedArgs = z.object(tool.schema).strict().parse(args ?? {});
293
+ }
294
+ catch (validationError) {
295
+ if (validationError instanceof ZodError) {
296
+ return validationErrorResult(name, validationError);
297
+ }
298
+ throw validationError;
299
+ }
109
300
  }
110
301
  logger.toolExecution(name, validatedArgs);
111
302
  const result = await tool.handler(validatedArgs);
112
- if (result && result.content)
113
- return result;
303
+ if (result?.isError) {
304
+ return normalizeToolErrorResult(name, result);
305
+ }
306
+ if (result && result.content) {
307
+ return normalizeSdkReadFailure(name, result);
308
+ }
114
309
  return {
115
- content: [{ type: "text", text: JSON.stringify({ status: "success", data: result }, (_, v) => typeof v === 'bigint' ? v.toString() : v, 2) }],
310
+ content: [{ type: "text", text: safeJsonStringify({ status: "success", data: result }) }],
116
311
  };
117
312
  }
118
313
  catch (error) {
119
314
  logger.error(`Error executing tool: ${name}`, error);
120
- return {
121
- content: [{ type: "text", text: `Error: ${error.message}` }],
122
- isError: true,
123
- };
315
+ const message = extractErrorMessage(error);
316
+ const code = inferToolErrorCode(message);
317
+ return errorResult({
318
+ tool: name,
319
+ code,
320
+ message,
321
+ hint: defaultHintForErrorCode(code, name),
322
+ action: "Fix inputs/prerequisites and retry.",
323
+ });
124
324
  }
125
325
  });
126
326
  // Resources Handlers
@@ -181,7 +381,7 @@ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
181
381
  async function main() {
182
382
  const transport = new StdioServerTransport();
183
383
  await server.connect(transport);
184
- logger.info("🚀 MYX Trading MCP Server v2.9.2 running (stdio, pure on-chain, prod ready)");
384
+ logger.info("🚀 MYX Trading MCP Server v3.0.0 running (stdio, pure on-chain, prod ready)");
185
385
  }
186
386
  main().catch((err) => {
187
387
  logger.error("Fatal Server Startup Error", err);
@@ -1,5 +1,65 @@
1
1
  import { getChainId } from "../auth/resolveClient.js";
2
2
  import { getMarketStateDesc } from "../utils/mappings.js";
3
+ function collectRows(input) {
4
+ if (Array.isArray(input))
5
+ return input.flatMap(collectRows);
6
+ if (!input || typeof input !== "object")
7
+ return [];
8
+ if (input.poolId || input.pool_id)
9
+ return [input];
10
+ return Object.values(input).flatMap(collectRows);
11
+ }
12
+ function extractMarketRows(raw, chainId) {
13
+ if (raw?.contractInfo && Array.isArray(raw.contractInfo.list)) {
14
+ return raw.contractInfo.list;
15
+ }
16
+ if (Array.isArray(raw?.data)) {
17
+ return raw.data;
18
+ }
19
+ if (raw?.data && Array.isArray(raw.data[chainId])) {
20
+ return raw.data[chainId];
21
+ }
22
+ if (Array.isArray(raw)) {
23
+ return raw;
24
+ }
25
+ return collectRows(raw?.data ?? raw);
26
+ }
27
+ function normalizeMarketState(value) {
28
+ if (value === undefined || value === null || value === "")
29
+ return null;
30
+ const casted = Number(value);
31
+ return Number.isFinite(casted) ? casted : null;
32
+ }
33
+ function normalizePoolId(row) {
34
+ const poolId = row?.poolId ?? row?.pool_id ?? "";
35
+ return String(poolId);
36
+ }
37
+ function matchesKeyword(row, keywordUpper) {
38
+ const poolId = normalizePoolId(row);
39
+ const haystack = [
40
+ row?.baseSymbol,
41
+ row?.quoteSymbol,
42
+ row?.baseQuoteSymbol,
43
+ row?.symbolName,
44
+ row?.name,
45
+ poolId,
46
+ row?.baseToken,
47
+ row?.quoteToken,
48
+ row?.marketId,
49
+ ]
50
+ .map((value) => String(value ?? "").toUpperCase())
51
+ .join("|");
52
+ return haystack.includes(keywordUpper);
53
+ }
54
+ async function fetchApiMarketRows(client) {
55
+ const marketListRes = await client.api.getMarketList().catch(() => null);
56
+ const marketRows = extractMarketRows(marketListRes, getChainId());
57
+ const marketRowsWithPoolId = marketRows.filter((row) => normalizePoolId(row));
58
+ if (marketRowsWithPoolId.length > 0)
59
+ return marketRowsWithPoolId;
60
+ const poolListRes = await client.api.getPoolList().catch(() => null);
61
+ return collectRows(poolListRes?.data ?? poolListRes);
62
+ }
3
63
  export async function getMarketPrice(client, poolId, chainIdOverride) {
4
64
  const chainId = chainIdOverride ?? getChainId();
5
65
  const tickerRes = await client.markets.getTickerList({
@@ -15,28 +75,45 @@ export async function getOraclePrice(client, poolId, chainIdOverride) {
15
75
  }
16
76
  export async function searchMarket(client, keyword, limit = 1000) {
17
77
  const chainId = getChainId();
18
- const searchRes = await client.markets.searchMarket({ chainId, keyword, limit });
19
- const raw = searchRes;
78
+ const normalizedKeyword = String(keyword ?? "").trim();
79
+ const requestedLimit = Number.isFinite(limit) && limit > 0 ? Math.floor(limit) : 1000;
20
80
  let dataList = [];
21
- if (raw && raw.contractInfo && Array.isArray(raw.contractInfo.list)) {
22
- dataList = raw.contractInfo.list;
81
+ try {
82
+ const searchRes = await client.markets.searchMarket({ chainId, keyword: normalizedKeyword, limit: requestedLimit });
83
+ dataList = extractMarketRows(searchRes, chainId);
23
84
  }
24
- else if (raw && Array.isArray(raw.data)) {
25
- dataList = raw.data;
85
+ catch {
86
+ dataList = [];
26
87
  }
27
- else if (raw && raw.data && Array.isArray(raw.data[chainId])) {
28
- dataList = raw.data[chainId];
88
+ if (dataList.length === 0 || normalizedKeyword.length === 0) {
89
+ const fallbackRows = await fetchApiMarketRows(client);
90
+ if (fallbackRows.length > 0) {
91
+ dataList = fallbackRows;
92
+ }
29
93
  }
30
- else if (raw && Array.isArray(raw)) {
31
- dataList = raw;
94
+ const filteredRows = normalizedKeyword
95
+ ? dataList.filter((row) => matchesKeyword(row, normalizedKeyword.toUpperCase()))
96
+ : dataList;
97
+ const dedupedByPoolId = new Map();
98
+ for (const row of filteredRows) {
99
+ const poolId = normalizePoolId(row);
100
+ if (!poolId)
101
+ continue;
102
+ if (!dedupedByPoolId.has(poolId)) {
103
+ dedupedByPoolId.set(poolId, row);
104
+ }
32
105
  }
33
- // Filter tradable markets (state=2 => Active)
34
- const activeMarkets = dataList.filter(m => Number(m.state) === 2);
106
+ const activeMarkets = Array.from(dedupedByPoolId.values())
107
+ .filter((row) => {
108
+ const state = normalizeMarketState(row?.state ?? row?.poolState);
109
+ return state === null || state === 2;
110
+ })
111
+ .slice(0, requestedLimit);
35
112
  // Get tickers for these pools to get price and change24h
36
113
  let tickers = [];
37
114
  if (activeMarkets.length > 0) {
38
115
  try {
39
- const poolIds = activeMarkets.map(m => m.poolId);
116
+ const poolIds = activeMarkets.map((market) => normalizePoolId(market));
40
117
  const tickerRes = await client.markets.getTickerList({ chainId, poolIds });
41
118
  tickers = Array.isArray(tickerRes) ? tickerRes : (tickerRes?.data || []);
42
119
  }
@@ -44,17 +121,20 @@ export async function searchMarket(client, keyword, limit = 1000) {
44
121
  console.error("Failed to fetch tickers:", e);
45
122
  }
46
123
  }
47
- return activeMarkets.map(m => {
48
- const ticker = tickers.find(t => String(t.poolId).toLowerCase() === String(m.poolId).toLowerCase());
124
+ return activeMarkets.map((market) => {
125
+ const poolId = normalizePoolId(market);
126
+ const state = normalizeMarketState(market?.state ?? market?.poolState);
127
+ const ticker = tickers.find((t) => String(t.poolId).toLowerCase() === poolId.toLowerCase());
128
+ const symbol = market?.baseQuoteSymbol || [market?.baseSymbol, market?.quoteSymbol].filter(Boolean).join("/");
49
129
  return {
50
- symbol: m.baseQuoteSymbol || `${m.baseSymbol}/${m.quoteSymbol}`,
51
- name: m.symbolName || m.name,
52
- poolId: m.poolId,
130
+ symbol: symbol || market?.symbolName || poolId,
131
+ name: market?.symbolName || market?.name || symbol || poolId,
132
+ poolId,
53
133
  price: ticker ? ticker.price : "0",
54
134
  change24h: ticker ? ticker.change : "0",
55
- tvl: m.tvl || "0",
56
- state: m.state,
57
- stateDescription: getMarketStateDesc(m.state)
135
+ tvl: market?.tvl || "0",
136
+ state: state ?? "unknown",
137
+ stateDescription: state === null ? "Unknown" : getMarketStateDesc(state)
58
138
  };
59
139
  });
60
140
  }
@@ -128,7 +208,13 @@ export async function resolvePool(client, poolId, keyword) {
128
208
  const base = String(row?.baseSymbol ?? "").toUpperCase();
129
209
  const pair = String(row?.baseQuoteSymbol ?? "").toUpperCase();
130
210
  const id = String(row?.poolId ?? row?.pool_id ?? "").toUpperCase();
131
- return base === searchKey || pair.includes(searchKey) || id === searchKey;
211
+ const baseToken = String(row?.baseToken ?? row?.base_token ?? "").toUpperCase();
212
+ const quoteToken = String(row?.quoteToken ?? row?.quote_token ?? "").toUpperCase();
213
+ return base === searchKey ||
214
+ pair.includes(searchKey) ||
215
+ id === searchKey ||
216
+ baseToken === searchKey ||
217
+ quoteToken === searchKey;
132
218
  });
133
219
  if (match) {
134
220
  return String(match.poolId ?? match.pool_id);
@@ -1,40 +1,132 @@
1
1
  import { z } from "zod";
2
2
  import { resolveClient, getChainId } from "../auth/resolveClient.js";
3
+ import { getBalances, getMarginBalance } from "../services/balanceService.js";
3
4
  import { getTradeFlowTypeDesc } from "../utils/mappings.js";
4
- export const getAccountInfoTool = {
5
- name: "get_account_info",
6
- description: "Get full account information for a pool (freeMargin, quoteProfit, etc.). Essential for risk management.",
5
+ function readErrorMessage(error) {
6
+ if (error instanceof Error)
7
+ return error.message;
8
+ if (typeof error === "string")
9
+ return error;
10
+ if (error && typeof error === "object" && "message" in error) {
11
+ return String(error.message);
12
+ }
13
+ return String(error ?? "unknown error");
14
+ }
15
+ function assertSdkReadSuccess(result, actionName) {
16
+ if (!result || typeof result !== "object" || Array.isArray(result))
17
+ return;
18
+ if (!Object.prototype.hasOwnProperty.call(result, "code"))
19
+ return;
20
+ const code = Number(result.code);
21
+ if (!Number.isFinite(code) || code === 0)
22
+ return;
23
+ const payload = result.data;
24
+ const hasPayload = payload !== null &&
25
+ payload !== undefined &&
26
+ (!Array.isArray(payload) || payload.length > 0);
27
+ if (code > 0 && hasPayload) {
28
+ // Keep aligned with server-side read normalization:
29
+ // positive non-zero code with usable payload is warning-like.
30
+ return;
31
+ }
32
+ const message = String(result.msg ?? result.message ?? "unknown error");
33
+ throw new Error(`${actionName} failed: code=${code}, msg=${message}`);
34
+ }
35
+ function normalizeAccountInfo(result) {
36
+ if (!(result && result.code === 0 && Array.isArray(result.data))) {
37
+ return result;
38
+ }
39
+ const values = result.data;
40
+ return {
41
+ ...result,
42
+ data: {
43
+ freeMargin: values[0],
44
+ walletBalance: values[1],
45
+ freeBaseAmount: values[2],
46
+ baseProfit: values[3],
47
+ quoteProfit: values[4],
48
+ reservedAmount: values[5],
49
+ releaseTime: values[6],
50
+ tradeableMargin: (BigInt(values[0]) + BigInt(values[1]) + (BigInt(values[6]) === 0n ? BigInt(values[4]) : 0n)).toString(),
51
+ baseProfitStatus: "base token to be unlocked",
52
+ quoteProfitStatus: BigInt(values[6]) > 0n ? "quote token to be unlocked" : "quote token unlocked/available"
53
+ }
54
+ };
55
+ }
56
+ export const getAccountTool = {
57
+ name: "get_account",
58
+ description: "Unified account snapshot. Distinguishes wallet balance vs trading-account metrics. Provide poolId for full trading-account details.",
7
59
  schema: {
8
- poolId: z.string().describe("Pool ID"),
60
+ poolId: z.string().optional().describe("Optional Pool ID. Required to include trading-account metrics (account info + margin)."),
9
61
  },
10
62
  handler: async (args) => {
11
63
  try {
12
64
  const { client, address } = await resolveClient();
13
65
  const chainId = getChainId();
14
- const result = await client.account.getAccountInfo(chainId, address, args.poolId);
15
- // Map the raw array to a structured object for better usability
16
- let structuredData = result;
17
- if (result && result.code === 0 && Array.isArray(result.data)) {
18
- const d = result.data;
19
- structuredData = {
20
- ...result,
21
- data: {
22
- freeMargin: d[0],
23
- walletBalance: d[1],
24
- freeBaseAmount: d[2],
25
- baseProfit: d[3],
26
- quoteProfit: d[4],
27
- reservedAmount: d[5],
28
- releaseTime: d[6],
29
- // Business Logic:
30
- // tradeableMargin = freeMargin + walletBalance + (quoteProfit if releaseTime == 0)
31
- tradeableMargin: (BigInt(d[0]) + BigInt(d[1]) + (BigInt(d[6]) === 0n ? BigInt(d[4]) : 0n)).toString(),
32
- baseProfitStatus: "base token to be unlocked",
33
- quoteProfitStatus: BigInt(d[6]) > 0n ? "quote token to be unlocked" : "quote token unlocked/available"
34
- }
35
- };
66
+ const poolId = args.poolId?.trim();
67
+ const overview = {
68
+ meta: {
69
+ address,
70
+ chainId,
71
+ poolId: poolId ?? null,
72
+ partial: false,
73
+ },
74
+ wallet: {
75
+ scope: "wallet_balance",
76
+ description: "On-chain wallet quote-token balance (funds not yet deposited into trading account).",
77
+ quoteTokenBalance: null,
78
+ raw: null,
79
+ error: null,
80
+ },
81
+ tradingAccount: {
82
+ scope: "trading_account_balance",
83
+ description: poolId
84
+ ? "Trading-account metrics for the selected pool."
85
+ : "Provide poolId to include trading-account metrics (account info + margin).",
86
+ poolId: poolId ?? null,
87
+ accountInfo: null,
88
+ marginBalance: null,
89
+ error: null,
90
+ },
91
+ warnings: [],
92
+ };
93
+ try {
94
+ const walletBalances = await getBalances(client, address);
95
+ assertSdkReadSuccess(walletBalances, "getWalletQuoteTokenBalance");
96
+ overview.wallet.quoteTokenBalance = walletBalances?.data ?? walletBalances;
97
+ overview.wallet.raw = walletBalances;
98
+ }
99
+ catch (walletError) {
100
+ overview.meta.partial = true;
101
+ overview.wallet.error = readErrorMessage(walletError);
102
+ overview.warnings.push(`wallet section failed: ${overview.wallet.error}`);
103
+ }
104
+ if (poolId) {
105
+ try {
106
+ const [accountInfoRaw, marginBalanceRaw] = await Promise.all([
107
+ client.account.getAccountInfo(chainId, address, poolId),
108
+ getMarginBalance(client, address, poolId),
109
+ ]);
110
+ assertSdkReadSuccess(accountInfoRaw, "getAccountInfo");
111
+ assertSdkReadSuccess(marginBalanceRaw, "getAvailableMarginBalance");
112
+ overview.tradingAccount.accountInfo = normalizeAccountInfo(accountInfoRaw);
113
+ overview.tradingAccount.marginBalance = marginBalanceRaw;
114
+ }
115
+ catch (tradingError) {
116
+ overview.meta.partial = true;
117
+ overview.tradingAccount.error = readErrorMessage(tradingError);
118
+ overview.warnings.push(`tradingAccount section failed: ${overview.tradingAccount.error}`);
119
+ }
120
+ }
121
+ if (overview.warnings.length === 0) {
122
+ delete overview.warnings;
123
+ }
124
+ const hasWalletData = overview.wallet.quoteTokenBalance !== null;
125
+ const hasTradingData = overview.tradingAccount.accountInfo !== null || overview.tradingAccount.marginBalance !== null;
126
+ if (!hasWalletData && !hasTradingData) {
127
+ throw new Error("Failed to build account snapshot: wallet and trading-account sections both unavailable.");
36
128
  }
37
- return { content: [{ type: "text", text: JSON.stringify({ status: "success", data: structuredData }, (_, v) => typeof v === 'bigint' ? v.toString() : v, 2) }] };
129
+ return { content: [{ type: "text", text: JSON.stringify({ status: "success", data: overview }, (_, v) => typeof v === 'bigint' ? v.toString() : v, 2) }] };
38
130
  }
39
131
  catch (error) {
40
132
  return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
@@ -65,25 +157,3 @@ export const getTradeFlowTool = {
65
157
  }
66
158
  },
67
159
  };
68
- export const getMarginBalanceTool = {
69
- name: "get_margin_balance",
70
- description: "Get available margin balance for a specific pool.",
71
- schema: {
72
- poolId: z.string().describe("Pool ID"),
73
- },
74
- handler: async (args) => {
75
- try {
76
- const { client, address } = await resolveClient();
77
- const chainId = getChainId();
78
- const result = await client.account.getAvailableMarginBalance({
79
- poolId: args.poolId,
80
- chainId,
81
- address,
82
- });
83
- return { content: [{ type: "text", text: JSON.stringify({ status: "success", data: result }, (_, v) => typeof v === 'bigint' ? v.toString() : v, 2) }] };
84
- }
85
- catch (error) {
86
- return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
87
- }
88
- },
89
- };