@michaleffffff/mcp-trading-server 3.0.15 → 3.0.16
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 +7 -0
- package/README.md +2 -1
- package/TOOL_EXAMPLES.md +2 -0
- package/dist/server.js +3 -3
- package/dist/tools/executeTrade.js +6 -0
- package/dist/tools/openPositionSimple.js +36 -13
- package/dist/utils/address.js +12 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.0.16 - 2026-03-18
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- Improved MCP-side validation UX:
|
|
7
|
+
- `executionFeeToken` now fails early with a clear `INVALID_PARAM` when callers pass the zero address, and points users to the real pool `quoteToken`.
|
|
8
|
+
- `open_position_simple` no longer returns a generic numeric parse error when `collateralAmount` is omitted; it now explains that `collateralAmount` is still required and suggests an approximate value from `size`, `price`, and `leverage`.
|
|
9
|
+
|
|
3
10
|
## 3.0.15 - 2026-03-18
|
|
4
11
|
|
|
5
12
|
### Fixed
|
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ A production-ready MCP (Model Context Protocol) server for deep integration with
|
|
|
6
6
|
|
|
7
7
|
# Release Notes
|
|
8
8
|
|
|
9
|
-
- **Current release: 3.0.
|
|
9
|
+
- **Current release: 3.0.16**
|
|
10
10
|
- **SDK baseline**: `@myx-trade/sdk@^1.0.2` compatibility completed.
|
|
11
11
|
- **Refinement**: Consolidated 40+ specialized tools into ~26 high-level unified tools.
|
|
12
12
|
- **Improved UX**: Enhanced AI parameter parsing, automated unit conversion, and structured error reporting.
|
|
@@ -109,6 +109,7 @@ Use these conventions when generating tool arguments:
|
|
|
109
109
|
- `orderType`: `MARKET` / `LIMIT` / `STOP` / `CONDITIONAL`
|
|
110
110
|
- `timeInForce`: SDK `v1.0.2` currently supports `IOC` only, so use `0` or `"IOC"`
|
|
111
111
|
- `size`: base token quantity, not USD notional; expected order value is usually `collateralAmount * leverage`
|
|
112
|
+
- `executionFeeToken`: must be a real token address; zero address is rejected. Use the pool `quoteToken`
|
|
112
113
|
- Human units: `"100"` means 100 USDC or 100 token units depending on field
|
|
113
114
|
- Raw units: `"raw:1000000"` means exact on-chain integer units
|
|
114
115
|
|
package/TOOL_EXAMPLES.md
CHANGED
|
@@ -74,6 +74,7 @@ Recommended high-level entry tool.
|
|
|
74
74
|
|
|
75
75
|
`marketId` is optional on `open_position_simple`. If supplied, it is validated against the market resolved from `poolId` or `keyword`.
|
|
76
76
|
`size` is always the base-asset quantity, not the USD notional. For example, a 500 USD order at price 1200 implies `size ≈ 0.416666...`.
|
|
77
|
+
`collateralAmount` remains required on `open_position_simple`; if omitted, MCP now returns an actionable suggestion instead of a generic parse error.
|
|
77
78
|
|
|
78
79
|
Raw-units example:
|
|
79
80
|
|
|
@@ -111,6 +112,7 @@ Low-level increase-order tool when you want full control.
|
|
|
111
112
|
```
|
|
112
113
|
|
|
113
114
|
`timeInForce` should be `0` (or `"IOC"` in string form) for SDK `v1.0.2`.
|
|
115
|
+
`executionFeeToken` must be a real token address; do not pass the zero address. Use the pool `quoteToken`.
|
|
114
116
|
|
|
115
117
|
### `close_position`
|
|
116
118
|
Close or reduce a position. Use `ALL` for a full close.
|
package/dist/server.js
CHANGED
|
@@ -39,7 +39,7 @@ function safeJsonStringify(value) {
|
|
|
39
39
|
}
|
|
40
40
|
function inferToolErrorCode(message) {
|
|
41
41
|
const lower = message.toLowerCase();
|
|
42
|
-
if (lower.includes("required") || lower.includes("invalid") || lower.includes("must be") || lower.includes("unexpected") || lower.includes("unrecognized")) {
|
|
42
|
+
if (lower.includes("required") || lower.includes("invalid") || lower.includes("must be") || lower.includes("unexpected") || lower.includes("unrecognized") || lower.includes("zero address")) {
|
|
43
43
|
return "INVALID_PARAM";
|
|
44
44
|
}
|
|
45
45
|
if (lower.includes("insufficient") && (lower.includes("allowance") || lower.includes("approval"))) {
|
|
@@ -461,7 +461,7 @@ function zodSchemaToJsonSchema(zodSchema) {
|
|
|
461
461
|
};
|
|
462
462
|
}
|
|
463
463
|
// ─── MCP Server ───
|
|
464
|
-
const server = new Server({ name: "myx-mcp-trading-server", version: "3.0.
|
|
464
|
+
const server = new Server({ name: "myx-mcp-trading-server", version: "3.0.16" }, { capabilities: { tools: {}, resources: {}, prompts: {} } });
|
|
465
465
|
// List tools
|
|
466
466
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
467
467
|
return {
|
|
@@ -582,7 +582,7 @@ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
|
582
582
|
async function main() {
|
|
583
583
|
const transport = new StdioServerTransport();
|
|
584
584
|
await server.connect(transport);
|
|
585
|
-
logger.info("🚀 MYX Trading MCP Server v3.0.
|
|
585
|
+
logger.info("🚀 MYX Trading MCP Server v3.0.16 running (stdio, pure on-chain, prod ready)");
|
|
586
586
|
}
|
|
587
587
|
main().catch((err) => {
|
|
588
588
|
logger.error("Fatal Server Startup Error", err);
|
|
@@ -7,6 +7,7 @@ import { verifyTradeOutcome } from "../utils/verification.js";
|
|
|
7
7
|
import { mapDirection, mapOrderType, mapTriggerType } from "../utils/mappings.js";
|
|
8
8
|
import { extractErrorMessage } from "../utils/errorMessage.js";
|
|
9
9
|
import { parseUserUnits } from "../utils/units.js";
|
|
10
|
+
import { isZeroAddress } from "../utils/address.js";
|
|
10
11
|
const POSITION_ID_RE = /^$|^0x[0-9a-fA-F]{64}$/;
|
|
11
12
|
const ZERO_POSITION_ID_RE = /^0x0{64}$/i;
|
|
12
13
|
export const executeTradeTool = {
|
|
@@ -61,6 +62,11 @@ export const executeTradeTool = {
|
|
|
61
62
|
const mappedOrderType = mapOrderType(args.orderType);
|
|
62
63
|
const mappedTriggerType = args.triggerType !== undefined ? mapTriggerType(args.triggerType) : undefined;
|
|
63
64
|
const slippagePctNormalized = normalizeSlippagePct4dp(args.slippagePct);
|
|
65
|
+
if (args.executionFeeToken !== undefined && args.executionFeeToken !== null && String(args.executionFeeToken).trim() !== "") {
|
|
66
|
+
if (isZeroAddress(args.executionFeeToken)) {
|
|
67
|
+
throw new Error(`executionFeeToken cannot be zero address. Use the pool quoteToken address ${poolData.quoteToken}.`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
64
70
|
const collateralRaw = parseUserUnits(args.collateralAmount, quoteDecimals, "collateralAmount");
|
|
65
71
|
const sizeRaw = parseUserUnits(args.size, baseDecimals, "size");
|
|
66
72
|
const priceRaw = parseUserUnits(args.price, 30, "price");
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { OrderType } from "@myx-trade/sdk";
|
|
3
|
+
import { formatUnits } from "ethers";
|
|
3
4
|
import { resolveClient, getChainId } from "../auth/resolveClient.js";
|
|
4
5
|
import { resolvePool } from "../services/marketService.js";
|
|
5
6
|
import { openPosition } from "../services/tradeService.js";
|
|
6
|
-
import { normalizeAddress } from "../utils/address.js";
|
|
7
|
+
import { isZeroAddress, normalizeAddress } from "../utils/address.js";
|
|
7
8
|
import { finalizeMutationResult } from "../utils/mutationResult.js";
|
|
8
9
|
import { mapDirection, mapOrderType } from "../utils/mappings.js";
|
|
9
10
|
import { normalizeSlippagePct4dp, SLIPPAGE_PCT_4DP_DESC } from "../utils/slippage.js";
|
|
@@ -43,6 +44,7 @@ export const openPositionSimpleTool = {
|
|
|
43
44
|
direction: z.any().describe("0=LONG, 1=SHORT, or strings like 'BUY'/'SELL'/'LONG'/'SHORT'."),
|
|
44
45
|
collateralAmount: z.coerce
|
|
45
46
|
.string()
|
|
47
|
+
.optional()
|
|
46
48
|
.describe("Collateral. e.g. '100' (quoted in USDC) or 'raw:100000000'."),
|
|
47
49
|
leverage: z.coerce.number().int().positive().describe("Leverage (integer, e.g. 5, 10)."),
|
|
48
50
|
orderType: z.union([z.string(), z.number()]).optional().describe("MARKET, LIMIT, STOP (default MARKET). Strings allowed."),
|
|
@@ -113,21 +115,14 @@ export const openPositionSimpleTool = {
|
|
|
113
115
|
const defaultAssetClass = Number(poolLevelConfig?.levelConfig?.assetClass ?? 0);
|
|
114
116
|
// 3) Parse & validate primary inputs
|
|
115
117
|
const dir = mapDirection(args.direction);
|
|
116
|
-
const collateralRaw = parseUserUnits(args.collateralAmount, quoteDecimals, "collateralAmount");
|
|
117
|
-
const collateralRawBig = asBigint(collateralRaw, "collateralAmount");
|
|
118
|
-
if (collateralRawBig <= 0n)
|
|
119
|
-
throw new Error("collateralAmount must be > 0.");
|
|
120
|
-
const maxTradeAmountHuman = String(process.env.MAX_TRADE_AMOUNT ?? "").trim();
|
|
121
|
-
if (maxTradeAmountHuman) {
|
|
122
|
-
const maxTradeRaw = parseUserUnits(maxTradeAmountHuman, quoteDecimals, "MAX_TRADE_AMOUNT");
|
|
123
|
-
const maxTradeRawBig = asBigint(maxTradeRaw, "MAX_TRADE_AMOUNT");
|
|
124
|
-
if (collateralRawBig > maxTradeRawBig) {
|
|
125
|
-
throw new Error(`collateralAmount exceeds MAX_TRADE_AMOUNT (collateralRaw=${collateralRawBig.toString()} > maxRaw=${maxTradeRawBig.toString()}).`);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
118
|
const orderType = mapOrderType(args.orderType ?? 0);
|
|
129
119
|
const postOnly = Boolean(args.postOnly ?? false);
|
|
130
120
|
const slippagePct = normalizeSlippagePct4dp(args.slippagePct ?? "50");
|
|
121
|
+
if (args.executionFeeToken !== undefined && args.executionFeeToken !== null && String(args.executionFeeToken).trim() !== "") {
|
|
122
|
+
if (isZeroAddress(args.executionFeeToken)) {
|
|
123
|
+
throw new Error(`executionFeeToken cannot be zero address. Use the pool quoteToken address ${quoteToken}.`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
131
126
|
const executionFeeToken = normalizeAddress(args.executionFeeToken || quoteToken, "executionFeeToken");
|
|
132
127
|
// 4) Determine reference price (30 decimals)
|
|
133
128
|
let price30;
|
|
@@ -157,6 +152,15 @@ export const openPositionSimpleTool = {
|
|
|
157
152
|
const price30Big = asBigint(price30, "price");
|
|
158
153
|
if (price30Big <= 0n)
|
|
159
154
|
throw new Error("price must be > 0.");
|
|
155
|
+
const collateralInput = String(args.collateralAmount ?? "").trim();
|
|
156
|
+
let collateralRaw = "";
|
|
157
|
+
let collateralRawBig = 0n;
|
|
158
|
+
if (collateralInput) {
|
|
159
|
+
collateralRaw = parseUserUnits(collateralInput, quoteDecimals, "collateralAmount");
|
|
160
|
+
collateralRawBig = asBigint(collateralRaw, "collateralAmount");
|
|
161
|
+
if (collateralRawBig <= 0n)
|
|
162
|
+
throw new Error("collateralAmount must be > 0.");
|
|
163
|
+
}
|
|
160
164
|
// 5) Compute or parse size (base raw units)
|
|
161
165
|
let sizeRaw = "";
|
|
162
166
|
let sizeMeta = { source: "computed" };
|
|
@@ -166,6 +170,9 @@ export const openPositionSimpleTool = {
|
|
|
166
170
|
sizeMeta = { source: "user" };
|
|
167
171
|
}
|
|
168
172
|
else {
|
|
173
|
+
if (!collateralInput) {
|
|
174
|
+
throw new Error("Either collateralAmount or size is required for open_position_simple.");
|
|
175
|
+
}
|
|
169
176
|
const notionalQuoteRaw = collateralRawBig * BigInt(args.leverage);
|
|
170
177
|
const numerator = notionalQuoteRaw * pow10(30 + baseDecimals);
|
|
171
178
|
const denominator = price30Big * pow10(quoteDecimals);
|
|
@@ -176,6 +183,22 @@ export const openPositionSimpleTool = {
|
|
|
176
183
|
sizeRaw = computed.toString();
|
|
177
184
|
sizeMeta = { source: "computed", notionalQuoteRaw: notionalQuoteRaw.toString() };
|
|
178
185
|
}
|
|
186
|
+
const sizeRawBig = asBigint(sizeRaw, "size");
|
|
187
|
+
if (!collateralInput) {
|
|
188
|
+
const notionalQuoteRaw = (sizeRawBig * price30Big * pow10(quoteDecimals)) / pow10(baseDecimals + 30);
|
|
189
|
+
const suggestedCollateralRaw = notionalQuoteRaw / BigInt(args.leverage);
|
|
190
|
+
const suggestedCollateralHuman = formatUnits(suggestedCollateralRaw, quoteDecimals);
|
|
191
|
+
const notionalHuman = formatUnits(notionalQuoteRaw, quoteDecimals);
|
|
192
|
+
throw new Error(`collateralAmount is required for open_position_simple. Based on size=${userSize || formatUnits(sizeRawBig, baseDecimals)}, price=${priceMeta.human ?? formatUnits(price30Big, 30)}, leverage=${args.leverage}, implied order value is ≈${notionalHuman} quote and suggested collateralAmount is ≈${suggestedCollateralHuman}.`);
|
|
193
|
+
}
|
|
194
|
+
const maxTradeAmountHuman = String(process.env.MAX_TRADE_AMOUNT ?? "").trim();
|
|
195
|
+
if (maxTradeAmountHuman) {
|
|
196
|
+
const maxTradeRaw = parseUserUnits(maxTradeAmountHuman, quoteDecimals, "MAX_TRADE_AMOUNT");
|
|
197
|
+
const maxTradeRawBig = asBigint(maxTradeRaw, "MAX_TRADE_AMOUNT");
|
|
198
|
+
if (collateralRawBig > maxTradeRawBig) {
|
|
199
|
+
throw new Error(`collateralAmount exceeds MAX_TRADE_AMOUNT (collateralRaw=${collateralRawBig.toString()} > maxRaw=${maxTradeRawBig.toString()}).`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
179
202
|
// 6) Compute tradingFee (quote raw units)
|
|
180
203
|
let tradingFeeRaw = null;
|
|
181
204
|
let tradingFeeMeta = { source: "computed" };
|
package/dist/utils/address.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getAddress } from "ethers";
|
|
2
|
+
export const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
|
|
2
3
|
export function normalizeAddress(value, label = "address") {
|
|
3
4
|
const raw = String(value || "").trim();
|
|
4
5
|
if (!raw)
|
|
@@ -15,3 +16,14 @@ export function normalizeAddress(value, label = "address") {
|
|
|
15
16
|
}
|
|
16
17
|
}
|
|
17
18
|
}
|
|
19
|
+
export function isZeroAddress(value) {
|
|
20
|
+
const raw = String(value ?? "").trim();
|
|
21
|
+
if (!raw)
|
|
22
|
+
return false;
|
|
23
|
+
try {
|
|
24
|
+
return getAddress(raw).toLowerCase() === ZERO_ADDRESS;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return raw.toLowerCase() === ZERO_ADDRESS;
|
|
28
|
+
}
|
|
29
|
+
}
|