@michaleffffff/mcp-trading-server 2.9.9 → 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.
- package/CHANGELOG.md +22 -0
- package/README.md +101 -5
- package/TOOL_EXAMPLES.md +536 -0
- package/dist/prompts/tradingGuide.js +7 -4
- package/dist/server.js +215 -15
- package/dist/services/marketService.js +101 -21
- package/dist/tools/accountInfo.js +119 -49
- package/dist/tools/index.js +1 -2
- package/dist/tools/searchMarket.js +3 -3
- package/package.json +5 -2
- package/dist/tools/getBalances.js +0 -17
|
@@ -12,24 +12,27 @@ export const tradingGuidePrompt = {
|
|
|
12
12
|
content: {
|
|
13
13
|
type: "text",
|
|
14
14
|
text: `
|
|
15
|
-
# MYX Trading MCP Best Practices (
|
|
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 (
|
|
21
|
-
2. **Context**: Use \`get_market_price\` and \`
|
|
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").
|
|
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: "
|
|
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
|
-
|
|
102
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
|
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
|
|
19
|
-
const
|
|
78
|
+
const normalizedKeyword = String(keyword ?? "").trim();
|
|
79
|
+
const requestedLimit = Number.isFinite(limit) && limit > 0 ? Math.floor(limit) : 1000;
|
|
20
80
|
let dataList = [];
|
|
21
|
-
|
|
22
|
-
|
|
81
|
+
try {
|
|
82
|
+
const searchRes = await client.markets.searchMarket({ chainId, keyword: normalizedKeyword, limit: requestedLimit });
|
|
83
|
+
dataList = extractMarketRows(searchRes, chainId);
|
|
23
84
|
}
|
|
24
|
-
|
|
25
|
-
dataList =
|
|
85
|
+
catch {
|
|
86
|
+
dataList = [];
|
|
26
87
|
}
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
31
|
-
dataList
|
|
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
|
-
|
|
34
|
-
|
|
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(
|
|
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(
|
|
48
|
-
const
|
|
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:
|
|
51
|
-
name:
|
|
52
|
-
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:
|
|
56
|
-
state:
|
|
57
|
-
stateDescription: getMarketStateDesc(
|
|
135
|
+
tvl: market?.tvl || "0",
|
|
136
|
+
state: state ?? "unknown",
|
|
137
|
+
stateDescription: state === null ? "Unknown" : getMarketStateDesc(state)
|
|
58
138
|
};
|
|
59
139
|
});
|
|
60
140
|
}
|
|
@@ -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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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:
|
|
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
|
-
};
|
package/dist/tools/index.js
CHANGED
|
@@ -28,10 +28,9 @@ export { createPerpMarketTool } from "./createPerpMarket.js";
|
|
|
28
28
|
export { manageLiquidityTool, getLpPriceTool } from "./manageLiquidity.js";
|
|
29
29
|
// Tools — 账户 & 查询
|
|
30
30
|
export { getPositionsTool } from "./getPositions.js";
|
|
31
|
-
export { getBalancesTool } from "./getBalances.js";
|
|
32
31
|
export { getOpenOrdersTool, getOrderHistoryTool } from "./orderQueries.js";
|
|
33
32
|
export { getPositionHistoryTool } from "./positionHistory.js";
|
|
34
|
-
export {
|
|
33
|
+
export { getAccountTool, getTradeFlowTool } from "./accountInfo.js";
|
|
35
34
|
export { getAccountVipInfoTool } from "./getAccountVipInfo.js";
|
|
36
35
|
export { accountDepositTool, accountWithdrawTool } from "./accountTransfer.js";
|
|
37
36
|
export { getMarketListTool } from "./getMarketList.js";
|