flash-trade-mcp 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +298 -84
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -21063,7 +21063,7 @@ var require_fallback = __commonJS((exports, module) => {
21063
21063
 
21064
21064
  // node_modules/bufferutil/index.js
21065
21065
  var require_bufferutil = __commonJS((exports, module) => {
21066
- var __dirname = "/Users/kalebrupe/Desktop/flash-trade-MCP/mcp/node_modules/bufferutil";
21066
+ var __dirname = "/home/runner/work/flash-trade-MCP/flash-trade-MCP/mcp/node_modules/bufferutil";
21067
21067
  try {
21068
21068
  module.exports = require_node_gyp_build2()(__dirname);
21069
21069
  } catch (e) {
@@ -21479,7 +21479,7 @@ var require_fallback2 = __commonJS((exports, module) => {
21479
21479
 
21480
21480
  // node_modules/utf-8-validate/index.js
21481
21481
  var require_utf_8_validate = __commonJS((exports, module) => {
21482
- var __dirname = "/Users/kalebrupe/Desktop/flash-trade-MCP/mcp/node_modules/utf-8-validate";
21482
+ var __dirname = "/home/runner/work/flash-trade-MCP/flash-trade-MCP/mcp/node_modules/utf-8-validate";
21483
21483
  try {
21484
21484
  module.exports = require_node_gyp_build2()(__dirname);
21485
21485
  } catch (e) {
@@ -49053,6 +49053,15 @@ function loadConfig() {
49053
49053
  if (!apiBaseUrl) {
49054
49054
  throw new Error("FLASH_API_URL environment variable is required");
49055
49055
  }
49056
+ let parsed;
49057
+ try {
49058
+ parsed = new URL(apiBaseUrl);
49059
+ } catch {
49060
+ throw new Error(`FLASH_API_URL is not a valid URL: ${apiBaseUrl}`);
49061
+ }
49062
+ if (parsed.protocol !== "https:") {
49063
+ console.error(`[flash-trade-mcp] WARNING: FLASH_API_URL uses ${parsed.protocol} — HTTPS is strongly recommended for production`);
49064
+ }
49056
49065
  return {
49057
49066
  apiBaseUrl: apiBaseUrl.replace(/\/$/, ""),
49058
49067
  timeoutMs: parseInt(process.env.FLASH_API_TIMEOUT ?? "30000", 10),
@@ -49069,8 +49078,8 @@ class FlashApiError extends Error {
49069
49078
  super(`Flash API error [${statusCode}] ${endpoint}: ${message}`);
49070
49079
  this.statusCode = statusCode;
49071
49080
  this.endpoint = endpoint;
49072
- this.responseBody = responseBody;
49073
49081
  this.name = "FlashApiError";
49082
+ this.responseBody = responseBody?.slice(0, 500);
49074
49083
  }
49075
49084
  }
49076
49085
 
@@ -49137,51 +49146,51 @@ class FlashApiClient {
49137
49146
  return this.get("/markets");
49138
49147
  }
49139
49148
  async getMarket(pubkey) {
49140
- return this.get(`/markets/${pubkey}`);
49149
+ return this.get(`/markets/${encodeURIComponent(pubkey)}`);
49141
49150
  }
49142
49151
  async getPools() {
49143
49152
  return this.get("/pools");
49144
49153
  }
49145
49154
  async getPool(pubkey) {
49146
- return this.get(`/pools/${pubkey}`);
49155
+ return this.get(`/pools/${encodeURIComponent(pubkey)}`);
49147
49156
  }
49148
49157
  async getCustodies() {
49149
49158
  return this.get("/custodies");
49150
49159
  }
49151
49160
  async getCustody(pubkey) {
49152
- return this.get(`/custodies/${pubkey}`);
49161
+ return this.get(`/custodies/${encodeURIComponent(pubkey)}`);
49153
49162
  }
49154
49163
  async getPrices() {
49155
49164
  return this.get("/prices");
49156
49165
  }
49157
49166
  async getPrice(symbol2) {
49158
- return this.get(`/prices/${symbol2}`);
49167
+ return this.get(`/prices/${encodeURIComponent(symbol2)}`);
49159
49168
  }
49160
49169
  async getPositions(owner) {
49161
- const query = owner ? `?owner=${owner}` : "";
49170
+ const query = owner ? `?owner=${encodeURIComponent(owner)}` : "";
49162
49171
  return this.get(`/positions${query}`);
49163
49172
  }
49164
49173
  async getPosition(pubkey) {
49165
- return this.get(`/positions/${pubkey}`);
49174
+ return this.get(`/positions/${encodeURIComponent(pubkey)}`);
49166
49175
  }
49167
49176
  async getOwnerPositions(owner, includePnlInLeverage = false) {
49168
- return this.get(`/positions/owner/${owner}?includePnlInLeverageDisplay=${includePnlInLeverage}`);
49177
+ return this.get(`/positions/owner/${encodeURIComponent(owner)}?includePnlInLeverageDisplay=${includePnlInLeverage}`);
49169
49178
  }
49170
49179
  async getOrders(owner) {
49171
- const query = owner ? `?owner=${owner}` : "";
49180
+ const query = owner ? `?owner=${encodeURIComponent(owner)}` : "";
49172
49181
  return this.get(`/orders${query}`);
49173
49182
  }
49174
49183
  async getOrder(pubkey) {
49175
- return this.get(`/orders/${pubkey}`);
49184
+ return this.get(`/orders/${encodeURIComponent(pubkey)}`);
49176
49185
  }
49177
49186
  async getOwnerOrders(owner) {
49178
- return this.get(`/orders/owner/${owner}`);
49187
+ return this.get(`/orders/owner/${encodeURIComponent(owner)}`);
49179
49188
  }
49180
49189
  async getPoolData() {
49181
49190
  return this.get("/pool-data");
49182
49191
  }
49183
49192
  async getPoolSnapshot(poolPubkey) {
49184
- return this.get(`/pool-data/${poolPubkey}`);
49193
+ return this.get(`/pool-data/${encodeURIComponent(poolPubkey)}`);
49185
49194
  }
49186
49195
  async openPosition(req) {
49187
49196
  return this.post("/transaction-builder/open-position", req);
@@ -49198,6 +49207,18 @@ class FlashApiClient {
49198
49207
  async reversePosition(req) {
49199
49208
  return this.post("/transaction-builder/reverse-position", req);
49200
49209
  }
49210
+ async placeTriggerOrder(req) {
49211
+ return this.post("/transaction-builder/place-trigger-order", req);
49212
+ }
49213
+ async editTriggerOrder(req) {
49214
+ return this.post("/transaction-builder/edit-trigger-order", req);
49215
+ }
49216
+ async cancelTriggerOrder(req) {
49217
+ return this.post("/transaction-builder/cancel-trigger-order", req);
49218
+ }
49219
+ async cancelAllTriggerOrders(req) {
49220
+ return this.post("/transaction-builder/cancel-all-trigger-orders", req);
49221
+ }
49201
49222
  async previewLimitOrderFees(req) {
49202
49223
  return this.post("/preview/limit-order-fees", req);
49203
49224
  }
@@ -49220,8 +49241,9 @@ function registerHealthTools(server, client) {
49220
49241
  const health = await client.getHealth();
49221
49242
  const lines = [`Status: ${health.status}`];
49222
49243
  for (const [key, val] of Object.entries(health)) {
49223
- if (key !== "status")
49224
- lines.push(`${key}: ${val}`);
49244
+ if (key !== "status") {
49245
+ lines.push(`${key}: ${typeof val === "object" ? JSON.stringify(val) : val}`);
49246
+ }
49225
49247
  }
49226
49248
  return { content: [{ type: "text", text: lines.join(`
49227
49249
  `) }] };
@@ -49307,7 +49329,7 @@ function registerMarketTools(server, client) {
49307
49329
  });
49308
49330
  server.registerTool("get_market", {
49309
49331
  description: "Get detailed information about a specific market by its on-chain account pubkey. Returns full market configuration including permissions and collective position data.",
49310
- inputSchema: { pubkey: exports_external.string().describe("Solana pubkey of the market account") }
49332
+ inputSchema: { pubkey: exports_external.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/).describe("Solana pubkey of the market account") }
49311
49333
  }, async ({ pubkey }) => {
49312
49334
  const market = await client.getMarket(pubkey);
49313
49335
  return { content: [{ type: "text", text: JSON.stringify(market, null, 2) }] };
@@ -49320,11 +49342,25 @@ function registerPoolTools(server, client) {
49320
49342
  description: "List all liquidity pools on Flash Trade. Pools hold the collateral that backs perpetual positions. Returns pool addresses and configuration."
49321
49343
  }, async () => {
49322
49344
  const pools = await client.getPools();
49323
- return { content: [{ type: "text", text: JSON.stringify(pools, null, 2) }] };
49345
+ const lines = [
49346
+ `${pools.length} pools available:
49347
+ `,
49348
+ "Pool Name | Custodies | Pubkey",
49349
+ "---------------|-----------|----------------------------------------------"
49350
+ ];
49351
+ for (const p of pools) {
49352
+ const name = (p.account?.name ?? "Unknown").padEnd(15);
49353
+ const custodyCount = String(p.account?.custodies?.length ?? 0).padEnd(10);
49354
+ lines.push(`${name}| ${custodyCount}| ${p.pubkey}`);
49355
+ }
49356
+ lines.push(`
49357
+ Use get_pool with a pubkey for full details. Use get_pool_data for AUM and utilization metrics.`);
49358
+ return { content: [{ type: "text", text: lines.join(`
49359
+ `) }] };
49324
49360
  });
49325
49361
  server.registerTool("get_pool", {
49326
49362
  description: "Get detailed information about a specific pool by its on-chain account pubkey.",
49327
- inputSchema: { pubkey: exports_external.string().describe("Solana pubkey of the pool account") }
49363
+ inputSchema: { pubkey: exports_external.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/).describe("Solana pubkey of the pool account") }
49328
49364
  }, async ({ pubkey }) => {
49329
49365
  const pool = await client.getPool(pubkey);
49330
49366
  return { content: [{ type: "text", text: JSON.stringify(pool, null, 2) }] };
@@ -49341,7 +49377,7 @@ function registerCustodyTools(server, client) {
49341
49377
  });
49342
49378
  server.registerTool("get_custody", {
49343
49379
  description: "Get detailed custody information for a specific custody account. Includes utilization, fees, and limits.",
49344
- inputSchema: { pubkey: exports_external.string().describe("Solana pubkey of the custody account") }
49380
+ inputSchema: { pubkey: exports_external.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/).describe("Solana pubkey of the custody account") }
49345
49381
  }, async ({ pubkey }) => {
49346
49382
  const custody = await client.getCustody(pubkey);
49347
49383
  return { content: [{ type: "text", text: JSON.stringify(custody, null, 2) }] };
@@ -49366,18 +49402,18 @@ function registerPriceTools(server, client) {
49366
49402
  });
49367
49403
  server.registerTool("get_price", {
49368
49404
  description: 'Get the current oracle price for a specific asset symbol (e.g., "SOL", "BTC", "ETH"). Case-insensitive. Returns price in USD with timestamp.',
49369
- inputSchema: { symbol: exports_external.string().describe('Asset symbol, e.g. "SOL", "BTC", "ETH"') }
49405
+ inputSchema: { symbol: exports_external.string().max(16).describe('Asset symbol, e.g. "SOL", "BTC", "ETH"') }
49370
49406
  }, async ({ symbol: symbol2 }) => {
49371
49407
  const data = await client.getPrice(symbol2);
49372
49408
  const usd = formatPrice(data.price, data.exponent);
49373
- return {
49374
- content: [{
49375
- type: "text",
49376
- text: `${symbol2.toUpperCase()}: $${usd}
49377
- Raw: ${data.price} (exp: ${data.exponent})
49378
- Timestamp: ${data.timestamp}`
49379
- }]
49380
- };
49409
+ const lines = [
49410
+ `${symbol2.toUpperCase()}: $${usd}`,
49411
+ `Raw: ${data.price} (exp: ${data.exponent})`
49412
+ ];
49413
+ if (data.timestamp)
49414
+ lines.push(`Timestamp: ${data.timestamp}`);
49415
+ return { content: [{ type: "text", text: lines.join(`
49416
+ `) }] };
49381
49417
  });
49382
49418
  }
49383
49419
 
@@ -49400,7 +49436,7 @@ function registerPositionTools(server, client) {
49400
49436
  server.registerTool("get_positions", {
49401
49437
  description: "List perpetual positions, optionally filtered by wallet owner. Without an owner filter, returns ALL open positions (may be large). With owner, returns enriched positions with computed PnL, leverage, and liquidation price.",
49402
49438
  inputSchema: {
49403
- owner: exports_external.string().optional().describe("Wallet pubkey to filter by. When provided, returns enriched positions with PnL, leverage, and liquidation price.")
49439
+ owner: exports_external.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/).optional().describe("Wallet pubkey to filter by. When provided, returns enriched positions with PnL, leverage, and liquidation price.")
49404
49440
  }
49405
49441
  }, async ({ owner }) => {
49406
49442
  if (owner) {
@@ -49420,7 +49456,7 @@ ${text}` }] };
49420
49456
  });
49421
49457
  server.registerTool("get_position", {
49422
49458
  description: "Get a single position by its on-chain account pubkey. Returns raw position data. For enriched data (PnL, leverage, liq price), use get_positions with the owner filter instead.",
49423
- inputSchema: { pubkey: exports_external.string().describe("Position account pubkey") }
49459
+ inputSchema: { pubkey: exports_external.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/).describe("Position account pubkey") }
49424
49460
  }, async ({ pubkey }) => {
49425
49461
  const position = await client.getPosition(pubkey);
49426
49462
  return { content: [{ type: "text", text: JSON.stringify(position, null, 2) }] };
@@ -49446,7 +49482,7 @@ function registerOrderTools(server, client) {
49446
49482
  server.registerTool("get_orders", {
49447
49483
  description: "List open orders (limit orders, take-profit, stop-loss), optionally filtered by wallet owner. When owner is provided, returns enriched order data with computed trigger prices and sizes.",
49448
49484
  inputSchema: {
49449
- owner: exports_external.string().optional().describe("Wallet pubkey to filter by. When provided, returns enriched orders.")
49485
+ owner: exports_external.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/).optional().describe("Wallet pubkey to filter by. When provided, returns enriched orders.")
49450
49486
  }
49451
49487
  }, async ({ owner }) => {
49452
49488
  if (owner) {
@@ -49466,7 +49502,7 @@ ${text}` }] };
49466
49502
  });
49467
49503
  server.registerTool("get_order", {
49468
49504
  description: "Get a single order account by its on-chain pubkey.",
49469
- inputSchema: { pubkey: exports_external.string().describe("Order account pubkey") }
49505
+ inputSchema: { pubkey: exports_external.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/).describe("Order account pubkey") }
49470
49506
  }, async ({ pubkey }) => {
49471
49507
  const order = await client.getOrder(pubkey);
49472
49508
  return { content: [{ type: "text", text: JSON.stringify(order, null, 2) }] };
@@ -49478,15 +49514,33 @@ function registerPoolDataTools(server, client) {
49478
49514
  server.registerTool("get_pool_data", {
49479
49515
  description: "Get computed pool metrics including AUM (assets under management), LP token stats, custody ratios, and utilization. Data is cached and refreshed every 15 seconds. Provide a pool_pubkey for a specific pool, or omit for all pools.",
49480
49516
  inputSchema: {
49481
- pool_pubkey: exports_external.string().optional().describe("Specific pool pubkey. If omitted, returns all pool snapshots.")
49517
+ pool_pubkey: exports_external.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/).optional().describe("Specific pool pubkey. If omitted, returns all pool snapshots.")
49482
49518
  }
49483
49519
  }, async ({ pool_pubkey }) => {
49484
49520
  if (pool_pubkey) {
49485
49521
  const snapshot = await client.getPoolSnapshot(pool_pubkey);
49486
49522
  return { content: [{ type: "text", text: JSON.stringify(snapshot, null, 2) }] };
49487
49523
  }
49488
- const data = await client.getPoolData();
49489
- return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
49524
+ const raw = await client.getPoolData();
49525
+ const pools = raw.pools ?? raw;
49526
+ const lines = [
49527
+ `${pools.length} pools:
49528
+ `,
49529
+ "Pool | AUM | LP Price | Stable% | Custodies",
49530
+ "---------------|--------------|----------|---------|----------"
49531
+ ];
49532
+ for (const p of pools) {
49533
+ const name = (p.poolName ?? "Unknown").padEnd(15);
49534
+ const aum = `$${p.lpStats?.totalPoolValueUsd ?? "?"}`.padEnd(14);
49535
+ const lp = `$${p.lpStats?.lpPrice ?? "?"}`.padEnd(9);
49536
+ const stable = `${p.lpStats?.stableCoinPercentage ?? "?"}%`.padEnd(8);
49537
+ const custodies = (p.custodyStats ?? []).map((c) => c.symbol).join(", ");
49538
+ lines.push(`${name}| ${aum}| ${lp}| ${stable}| ${custodies}`);
49539
+ }
49540
+ lines.push(`
49541
+ Use get_pool_data with pool_pubkey for full custody stats.`);
49542
+ return { content: [{ type: "text", text: lines.join(`
49543
+ `) }] };
49490
49544
  });
49491
49545
  }
49492
49546
 
@@ -49536,17 +49590,17 @@ function registerOpenPositionTool(server, client) {
49536
49590
  server.registerTool("open_position", {
49537
49591
  description: "Build a transaction to open a new perpetual position on Flash Trade. Returns a preview (entry price, fees, leverage, liquidation price) AND an unsigned transaction. The transaction must be signed by the user's wallet and submitted to Solana separately. IMPORTANT: Always present the preview to the user before they sign. This tool does NOT execute the trade. Supports both MARKET and LIMIT orders, with optional take-profit and stop-loss. COLLATERAL WARNING: Limit orders, take-profit, and stop-loss require >$10 collateral AFTER entry fees. A $10 position will have fees deducted, dropping collateral below $10 and preventing TP/SL/limit orders. Use at least $11-12 input_amount when planning to set TP/SL.",
49538
49592
  inputSchema: {
49539
- input_token_symbol: exports_external.string().describe('Token to pay with: "USDC", "SOL", etc.'),
49540
- output_token_symbol: exports_external.string().describe('Market to trade: "SOL", "BTC", "ETH", etc.'),
49541
- input_amount: exports_external.string().describe('Amount of input token, e.g. "100.0"'),
49542
- leverage: exports_external.string().describe('Leverage multiplier, e.g. "5.0"'),
49593
+ input_token_symbol: exports_external.string().max(16).describe('Token to pay with: "USDC", "SOL", etc.'),
49594
+ output_token_symbol: exports_external.string().max(16).describe('Market to trade: "SOL", "BTC", "ETH", etc.'),
49595
+ input_amount: exports_external.string().max(32).describe('Amount of input token, e.g. "100.0"'),
49596
+ leverage: exports_external.string().max(8).describe('Leverage multiplier, e.g. "5.0"'),
49543
49597
  trade_type: exports_external.enum(["LONG", "SHORT"]).describe("Trade direction"),
49544
- owner: exports_external.string().describe("Wallet pubkey (required to build the transaction)"),
49598
+ owner: exports_external.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/).describe("Wallet pubkey (required to build the transaction)"),
49545
49599
  order_type: exports_external.enum(["MARKET", "LIMIT"]).optional().describe("Default: MARKET"),
49546
- limit_price: exports_external.string().optional().describe("Required for LIMIT orders, UI format price"),
49547
- slippage_percentage: exports_external.string().optional().describe('Default: "0.5" (0.5%)'),
49548
- take_profit: exports_external.string().optional().describe("TP trigger price in UI format"),
49549
- stop_loss: exports_external.string().optional().describe("SL trigger price in UI format"),
49600
+ limit_price: exports_external.string().max(32).optional().describe("Required for LIMIT orders, UI format price"),
49601
+ slippage_percentage: exports_external.string().max(8).optional().describe('Default: "0.5" (0.5%)'),
49602
+ take_profit: exports_external.string().max(32).optional().describe("TP trigger price in UI format"),
49603
+ stop_loss: exports_external.string().max(32).optional().describe("SL trigger price in UI format"),
49550
49604
  degen_mode: exports_external.boolean().optional().describe("Enable degen mode (higher leverage limits)")
49551
49605
  }
49552
49606
  }, async (params) => {
@@ -49564,12 +49618,21 @@ function registerOpenPositionTool(server, client) {
49564
49618
  stopLoss: params.stop_loss,
49565
49619
  degenMode: params.degen_mode
49566
49620
  });
49567
- const text = formatOpenPreview({
49621
+ let text = formatOpenPreview({
49568
49622
  outputTokenSymbol: params.output_token_symbol,
49569
49623
  tradeType: params.trade_type,
49570
49624
  inputTokenSymbol: params.input_token_symbol,
49571
49625
  inputAmountUi: params.input_amount
49572
49626
  }, res);
49627
+ if (params.take_profit || params.stop_loss) {
49628
+ const collateral = parseFloat(res.youPayUsdUi || "0");
49629
+ const fee = parseFloat(res.entryFee || "0");
49630
+ if (collateral - fee < 10) {
49631
+ text += `
49632
+
49633
+ WARNING: Collateral after fees is below $10. Take-profit and stop-loss orders require >$10 collateral and will FAIL on-chain. Use at least $11-12 input_amount.`;
49634
+ }
49635
+ }
49573
49636
  return { content: [{ type: "text", text }] };
49574
49637
  });
49575
49638
  }
@@ -49609,11 +49672,11 @@ function registerClosePositionTool(server, client) {
49609
49672
  server.registerTool("close_position", {
49610
49673
  description: "Build a transaction to close (fully or partially) an existing perpetual position. Returns a preview with PnL, fees, and receive amount, plus an unsigned transaction. For a full close, set input_usd to the position's full size. For a partial close, use a smaller amount. The transaction must be signed and submitted separately.",
49611
49674
  inputSchema: {
49612
- position_key: exports_external.string().describe("Position account pubkey to close"),
49613
- input_usd: exports_external.string().describe('USD amount to close, e.g. "500.00" for full or "250.00" for partial'),
49614
- withdraw_token_symbol: exports_external.string().describe('Token to receive: "USDC", "SOL", etc.'),
49675
+ position_key: exports_external.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/).describe("Position account pubkey to close"),
49676
+ input_usd: exports_external.string().max(32).describe('USD amount to close, e.g. "500.00" for full or "250.00" for partial'),
49677
+ withdraw_token_symbol: exports_external.string().max(16).describe('Token to receive: "USDC", "SOL", etc.'),
49615
49678
  keep_leverage_same: exports_external.boolean().optional().describe("Keep leverage constant during partial close"),
49616
- slippage_percentage: exports_external.string().optional().describe('Default: "0.5" (0.5%)')
49679
+ slippage_percentage: exports_external.string().max(8).optional().describe('Default: "0.5" (0.5%)')
49617
49680
  }
49618
49681
  }, async (params) => {
49619
49682
  const res = await client.closePosition({
@@ -49672,11 +49735,11 @@ function registerCollateralTools(server, client) {
49672
49735
  server.registerTool("add_collateral", {
49673
49736
  description: "Build a transaction to add collateral to an existing position. This reduces leverage and moves the liquidation price further from the current price (safer). Returns a preview and unsigned transaction.",
49674
49737
  inputSchema: {
49675
- position_key: exports_external.string().describe("Position account pubkey"),
49676
- deposit_amount: exports_external.string().describe("Amount to deposit in UI format"),
49677
- deposit_token_symbol: exports_external.string().describe('Token to deposit: "USDC", "SOL", etc.'),
49678
- owner: exports_external.string().describe("Wallet pubkey"),
49679
- slippage_percentage: exports_external.string().optional().describe('Default: "0.5" (0.5%)')
49738
+ position_key: exports_external.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/).describe("Position account pubkey"),
49739
+ deposit_amount: exports_external.string().max(32).describe("Amount to deposit in UI format"),
49740
+ deposit_token_symbol: exports_external.string().max(16).describe('Token to deposit: "USDC", "SOL", etc.'),
49741
+ owner: exports_external.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/).describe("Wallet pubkey"),
49742
+ slippage_percentage: exports_external.string().max(8).optional().describe('Default: "0.5" (0.5%)')
49680
49743
  }
49681
49744
  }, async (params) => {
49682
49745
  const res = await client.addCollateral({
@@ -49691,11 +49754,11 @@ function registerCollateralTools(server, client) {
49691
49754
  server.registerTool("remove_collateral", {
49692
49755
  description: "Build a transaction to remove collateral from an existing position. This increases leverage and moves the liquidation price closer (riskier). WARNING: Removing too much collateral can lead to liquidation. Returns a preview and unsigned transaction.",
49693
49756
  inputSchema: {
49694
- position_key: exports_external.string().describe("Position account pubkey"),
49695
- withdraw_amount_usd: exports_external.string().describe("USD amount to withdraw"),
49696
- withdraw_token_symbol: exports_external.string().describe('Token to receive: "USDC", "SOL", etc.'),
49697
- owner: exports_external.string().describe("Wallet pubkey"),
49698
- slippage_percentage: exports_external.string().optional().describe('Default: "0.5" (0.5%)')
49757
+ position_key: exports_external.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/).describe("Position account pubkey"),
49758
+ withdraw_amount_usd: exports_external.string().max(32).describe("USD amount to withdraw"),
49759
+ withdraw_token_symbol: exports_external.string().max(16).describe('Token to receive: "USDC", "SOL", etc.'),
49760
+ owner: exports_external.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/).describe("Wallet pubkey"),
49761
+ slippage_percentage: exports_external.string().max(8).optional().describe('Default: "0.5" (0.5%)')
49699
49762
  }
49700
49763
  }, async (params) => {
49701
49764
  const res = await client.removeCollateral({
@@ -49743,9 +49806,9 @@ function registerReversePositionTool(server, client) {
49743
49806
  server.registerTool("reverse_position", {
49744
49807
  description: "Build a transaction to reverse a position (close current + open opposite direction). For example, close a LONG and open a SHORT with the same collateral. Returns combined close+open preview and a single unsigned transaction.",
49745
49808
  inputSchema: {
49746
- position_key: exports_external.string().describe("Position account pubkey to reverse"),
49747
- owner: exports_external.string().describe("Wallet pubkey"),
49748
- slippage_percentage: exports_external.string().optional().describe('Default: "0.5" (0.5%)'),
49809
+ position_key: exports_external.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/).describe("Position account pubkey to reverse"),
49810
+ owner: exports_external.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/).describe("Wallet pubkey"),
49811
+ slippage_percentage: exports_external.string().max(8).optional().describe('Default: "0.5" (0.5%)'),
49749
49812
  degen_mode: exports_external.boolean().optional().describe("Enable degen mode for the new position")
49750
49813
  }
49751
49814
  }, async (params) => {
@@ -49764,11 +49827,11 @@ function registerPreviewTools(server, client) {
49764
49827
  server.registerTool("preview_limit_order_fees", {
49765
49828
  description: "Preview the entry price, fees, liquidation price, and borrow rate for a limit order BEFORE placing it. Use this to evaluate a trade before committing. No transaction is built.",
49766
49829
  inputSchema: {
49767
- market_symbol: exports_external.string().describe('Market symbol, e.g. "SOL", "BTC", "ETH"'),
49768
- input_amount: exports_external.string().describe("Collateral amount in UI format"),
49769
- output_amount: exports_external.string().describe("Position size in target token"),
49830
+ market_symbol: exports_external.string().max(16).describe('Market symbol, e.g. "SOL", "BTC", "ETH"'),
49831
+ input_amount: exports_external.string().max(32).describe("Collateral amount in UI format"),
49832
+ output_amount: exports_external.string().max(32).describe("Position size in target token"),
49770
49833
  side: exports_external.enum(["LONG", "SHORT"]).describe("Trade direction"),
49771
- limit_price: exports_external.string().optional().describe("Limit price; uses live price if omitted"),
49834
+ limit_price: exports_external.string().max(32).optional().describe("Limit price; uses live price if omitted"),
49772
49835
  trading_fee_discount_percent: exports_external.number().optional().describe("Fee discount from FAF staking (0-100)")
49773
49836
  }
49774
49837
  }, async (params) => {
@@ -49796,8 +49859,8 @@ WARNING: ${res.err}`);
49796
49859
  server.registerTool("preview_exit_fee", {
49797
49860
  description: "Preview the exit fee and exit price for closing a specific amount of a position. Use this to estimate close costs before calling close_position. No transaction is built.",
49798
49861
  inputSchema: {
49799
- position_key: exports_external.string().describe("Position account pubkey"),
49800
- close_amount_usd: exports_external.string().describe("USD amount to close")
49862
+ position_key: exports_external.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/).describe("Position account pubkey"),
49863
+ close_amount_usd: exports_external.string().max(32).describe("USD amount to close")
49801
49864
  }
49802
49865
  }, async (params) => {
49803
49866
  const res = await client.previewExitFee({
@@ -49819,14 +49882,14 @@ WARNING: ${res.err}`);
49819
49882
  description: 'Calculate take-profit or stop-loss prices and projected PnL. Three modes: "forward" (trigger price → PnL), "reverse_pnl" (target PnL → trigger price), "reverse_roi" (target ROI% → trigger price). Works for existing positions (by pubkey) or hypothetical orders (provide market_symbol, entry_price, size, collateral, side).',
49820
49883
  inputSchema: {
49821
49884
  mode: exports_external.enum(["forward", "reverse_pnl", "reverse_roi"]).describe("Calculation mode"),
49822
- position_key: exports_external.string().optional().describe("Position pubkey (for existing positions)"),
49823
- market_symbol: exports_external.string().optional().describe("Market symbol (for hypothetical orders)"),
49824
- entry_price: exports_external.string().optional().describe("Entry price (for hypothetical orders)"),
49825
- size_usd: exports_external.string().optional().describe("Position size USD (for hypothetical orders)"),
49826
- collateral_usd: exports_external.string().optional().describe("Collateral USD (for hypothetical orders)"),
49885
+ position_key: exports_external.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/).optional().describe("Position pubkey (for existing positions)"),
49886
+ market_symbol: exports_external.string().max(16).optional().describe("Market symbol (for hypothetical orders)"),
49887
+ entry_price: exports_external.string().max(32).optional().describe("Entry price (for hypothetical orders)"),
49888
+ size_usd: exports_external.string().max(32).optional().describe("Position size USD (for hypothetical orders)"),
49889
+ collateral_usd: exports_external.string().max(32).optional().describe("Collateral USD (for hypothetical orders)"),
49827
49890
  side: exports_external.enum(["LONG", "SHORT"]).optional().describe("Side (for hypothetical orders)"),
49828
- trigger_price: exports_external.string().optional().describe('Trigger price (required for "forward" mode)'),
49829
- target_pnl_usd: exports_external.string().optional().describe('Target PnL USD (required for "reverse_pnl" mode)'),
49891
+ trigger_price: exports_external.string().max(32).optional().describe('Trigger price (required for "forward" mode)'),
49892
+ target_pnl_usd: exports_external.string().max(32).optional().describe('Target PnL USD (required for "reverse_pnl" mode)'),
49830
49893
  target_roi_percent: exports_external.number().optional().describe('Target ROI% (required for "reverse_roi" mode)')
49831
49894
  }
49832
49895
  }, async (params) => {
@@ -49858,8 +49921,8 @@ WARNING: ${res.err}`);
49858
49921
  server.registerTool("preview_margin", {
49859
49922
  description: "Preview the effect of adding or removing margin (collateral) on a position. Shows new leverage, new liquidation price, and max adjustable amount. Use this before calling add_collateral or remove_collateral. No transaction is built.",
49860
49923
  inputSchema: {
49861
- position_key: exports_external.string().describe("Position account pubkey"),
49862
- margin_delta_usd: exports_external.string().describe("Amount in USD to add or remove"),
49924
+ position_key: exports_external.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/).describe("Position account pubkey"),
49925
+ margin_delta_usd: exports_external.string().max(32).describe("Amount in USD to add or remove"),
49863
49926
  action: exports_external.enum(["ADD", "REMOVE"]).describe("ADD to reduce leverage, REMOVE to increase leverage")
49864
49927
  }
49865
49928
  }, async (params) => {
@@ -56796,15 +56859,19 @@ var $VersionedTransaction = VersionedTransaction;
56796
56859
 
56797
56860
  // src/tools/sign-and-send.ts
56798
56861
  import fs from "node:fs";
56862
+
56863
+ // src/sanitize.ts
56799
56864
  function sanitizeError(e) {
56800
56865
  const msg = e instanceof Error ? e.message : String(e);
56801
- return msg.replace(/\[[\d,\s]{20,}\]/g, "[REDACTED]").replace(/[0-9a-fA-F]{40,}/g, "[REDACTED]").replace(/[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{40,}/g, "[REDACTED]");
56866
+ return msg.replace(/\[[\d,\s]{20,}\]/g, "[REDACTED]").replace(/[0-9a-fA-F]{40,}/g, "[REDACTED]").replace(/[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{40,}/g, "[REDACTED]").replace(/[A-Za-z0-9+/]{40,}={0,2}/g, "[REDACTED]");
56802
56867
  }
56868
+
56869
+ // src/tools/sign-and-send.ts
56803
56870
  function registerSignAndSendTool(server) {
56804
56871
  server.registerTool("sign_and_send", {
56805
56872
  description: "Sign and submit a base64-encoded unsigned Solana transaction using the locally configured keypair. " + "Call this AFTER a transaction tool (open_position, close_position, add_collateral, remove_collateral, reverse_position) " + "returns a transactionBase64 string AND the user has reviewed and approved the preview. " + "The keypair is read from KEYPAIR_PATH (default: ~/.config/solana/id.json). " + "Returns the confirmed transaction signature and a Solscan link. " + "IMPORTANT: Always show the transaction preview to the user and get their approval BEFORE calling this tool. " + "This tool signs with the local keypair and submits to Solana mainnet — the action is IRREVERSIBLE. " + "NOTE: This tool never exposes private key material in its output.",
56806
56873
  inputSchema: {
56807
- transaction_base64: exports_external.string().describe("The base64-encoded unsigned transaction returned by a transaction tool")
56874
+ transaction_base64: exports_external.string().max(1e4).describe("The base64-encoded unsigned transaction returned by a transaction tool")
56808
56875
  }
56809
56876
  }, async (params) => {
56810
56877
  const rpcUrl = process.env.SOLANA_RPC_URL ?? "https://api.mainnet-beta.solana.com";
@@ -56880,6 +56947,152 @@ Signature: ${signature2}` }],
56880
56947
  });
56881
56948
  }
56882
56949
 
56950
+ // src/tools/trigger-orders.ts
56951
+ function registerTriggerOrderTools(server, client) {
56952
+ server.registerTool("place_trigger_order", {
56953
+ description: "Place a take-profit (TP) or stop-loss (SL) trigger order on an existing position. The trigger order will automatically close part or all of the position when the price hits the trigger level. Requires an open position for the given market/side. Returns an unsigned transaction. Up to 5 trigger orders per position.",
56954
+ inputSchema: {
56955
+ market_symbol: exports_external.string().max(16).describe('Market symbol, e.g. "SOL", "BTC", "ETH"'),
56956
+ collateral_symbol: exports_external.string().max(16).describe('Collateral token, e.g. "USDC", "JITOSOL"'),
56957
+ side: exports_external.enum(["LONG", "SHORT"]).describe("Position side"),
56958
+ trigger_price: exports_external.string().max(32).describe('Trigger price in UI format, e.g. "160.00"'),
56959
+ size_amount: exports_external.string().max(32).describe('Size in target token to close when triggered, e.g. "0.5"'),
56960
+ is_stop_loss: exports_external.boolean().describe("true = stop-loss, false = take-profit"),
56961
+ owner: exports_external.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/).describe("Wallet pubkey (must own the position)")
56962
+ }
56963
+ }, async (params) => {
56964
+ const res = await client.placeTriggerOrder({
56965
+ marketSymbol: params.market_symbol,
56966
+ collateralSymbol: params.collateral_symbol,
56967
+ side: params.side,
56968
+ triggerPriceUi: params.trigger_price,
56969
+ sizeAmountUi: params.size_amount,
56970
+ isStopLoss: params.is_stop_loss,
56971
+ owner: params.owner
56972
+ });
56973
+ const lines = [
56974
+ `=== Place ${params.is_stop_loss ? "Stop-Loss" : "Take-Profit"} Order ===`,
56975
+ `Market: ${params.market_symbol} ${params.side}`,
56976
+ `Trigger: $${params.trigger_price}`,
56977
+ `Size: ${params.size_amount} ${params.market_symbol}`
56978
+ ];
56979
+ if (res.err)
56980
+ lines.push(`
56981
+ WARNING: ${res.err}`);
56982
+ if (res.transactionBase64) {
56983
+ lines.push(`
56984
+ Transaction (base64, unsigned — sign with wallet):`);
56985
+ lines.push(res.transactionBase64);
56986
+ }
56987
+ return { content: [{ type: "text", text: lines.join(`
56988
+ `) }] };
56989
+ });
56990
+ server.registerTool("edit_trigger_order", {
56991
+ description: "Edit an existing take-profit or stop-loss trigger order. Change the trigger price, size, or order type without canceling and re-placing. Requires the order_id (0-7) from get_orders. Returns an unsigned transaction.",
56992
+ inputSchema: {
56993
+ market_symbol: exports_external.string().max(16).describe('Market symbol, e.g. "SOL", "BTC", "ETH"'),
56994
+ collateral_symbol: exports_external.string().max(16).describe('Collateral token, e.g. "USDC", "JITOSOL"'),
56995
+ side: exports_external.enum(["LONG", "SHORT"]).describe("Position side"),
56996
+ order_id: exports_external.number().describe("Index of the trigger order to edit (0-7)"),
56997
+ trigger_price: exports_external.string().max(32).describe("New trigger price in UI format"),
56998
+ size_amount: exports_external.string().max(32).describe("New size in target token"),
56999
+ is_stop_loss: exports_external.boolean().describe("true = stop-loss, false = take-profit"),
57000
+ owner: exports_external.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/).describe("Wallet pubkey (must be original order owner)")
57001
+ }
57002
+ }, async (params) => {
57003
+ const res = await client.editTriggerOrder({
57004
+ marketSymbol: params.market_symbol,
57005
+ collateralSymbol: params.collateral_symbol,
57006
+ side: params.side,
57007
+ orderId: params.order_id,
57008
+ triggerPriceUi: params.trigger_price,
57009
+ sizeAmountUi: params.size_amount,
57010
+ isStopLoss: params.is_stop_loss,
57011
+ owner: params.owner
57012
+ });
57013
+ const lines = [
57014
+ `=== Edit ${params.is_stop_loss ? "Stop-Loss" : "Take-Profit"} Order #${params.order_id} ===`,
57015
+ `Market: ${params.market_symbol} ${params.side}`,
57016
+ `New Trigger: $${params.trigger_price}`,
57017
+ `New Size: ${params.size_amount} ${params.market_symbol}`
57018
+ ];
57019
+ if (res.err)
57020
+ lines.push(`
57021
+ WARNING: ${res.err}`);
57022
+ if (res.transactionBase64) {
57023
+ lines.push(`
57024
+ Transaction (base64, unsigned — sign with wallet):`);
57025
+ lines.push(res.transactionBase64);
57026
+ }
57027
+ return { content: [{ type: "text", text: lines.join(`
57028
+ `) }] };
57029
+ });
57030
+ server.registerTool("cancel_trigger_order", {
57031
+ description: "Cancel a single take-profit or stop-loss trigger order. Requires the order_id (0-7) from get_orders. Returns an unsigned transaction.",
57032
+ inputSchema: {
57033
+ market_symbol: exports_external.string().max(16).describe('Market symbol, e.g. "SOL", "BTC", "ETH"'),
57034
+ collateral_symbol: exports_external.string().max(16).describe('Collateral token, e.g. "USDC", "JITOSOL"'),
57035
+ side: exports_external.enum(["LONG", "SHORT"]).describe("Position side"),
57036
+ order_id: exports_external.number().describe("Index of the trigger order to cancel (0-7)"),
57037
+ is_stop_loss: exports_external.boolean().describe("true = stop-loss, false = take-profit"),
57038
+ owner: exports_external.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/).describe("Wallet pubkey (must own the order)")
57039
+ }
57040
+ }, async (params) => {
57041
+ const res = await client.cancelTriggerOrder({
57042
+ marketSymbol: params.market_symbol,
57043
+ collateralSymbol: params.collateral_symbol,
57044
+ side: params.side,
57045
+ orderId: params.order_id,
57046
+ isStopLoss: params.is_stop_loss,
57047
+ owner: params.owner
57048
+ });
57049
+ const lines = [
57050
+ `=== Cancel ${params.is_stop_loss ? "Stop-Loss" : "Take-Profit"} Order #${params.order_id} ===`,
57051
+ `Market: ${params.market_symbol} ${params.side}`
57052
+ ];
57053
+ if (res.err)
57054
+ lines.push(`
57055
+ WARNING: ${res.err}`);
57056
+ if (res.transactionBase64) {
57057
+ lines.push(`
57058
+ Transaction (base64, unsigned — sign with wallet):`);
57059
+ lines.push(res.transactionBase64);
57060
+ }
57061
+ return { content: [{ type: "text", text: lines.join(`
57062
+ `) }] };
57063
+ });
57064
+ server.registerTool("cancel_all_trigger_orders", {
57065
+ description: "Cancel ALL take-profit and stop-loss trigger orders for a market+side. Removes all TP and SL orders in one transaction. Returns an unsigned transaction.",
57066
+ inputSchema: {
57067
+ market_symbol: exports_external.string().max(16).describe('Market symbol, e.g. "SOL", "BTC", "ETH"'),
57068
+ collateral_symbol: exports_external.string().max(16).describe('Collateral token, e.g. "USDC", "JITOSOL"'),
57069
+ side: exports_external.enum(["LONG", "SHORT"]).describe("Position side"),
57070
+ owner: exports_external.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/).describe("Wallet pubkey (must own the orders)")
57071
+ }
57072
+ }, async (params) => {
57073
+ const res = await client.cancelAllTriggerOrders({
57074
+ marketSymbol: params.market_symbol,
57075
+ collateralSymbol: params.collateral_symbol,
57076
+ side: params.side,
57077
+ owner: params.owner
57078
+ });
57079
+ const lines = [
57080
+ `=== Cancel All Trigger Orders ===`,
57081
+ `Market: ${params.market_symbol} ${params.side}`
57082
+ ];
57083
+ if (res.err)
57084
+ lines.push(`
57085
+ WARNING: ${res.err}`);
57086
+ if (res.transactionBase64) {
57087
+ lines.push(`
57088
+ Transaction (base64, unsigned — sign with wallet):`);
57089
+ lines.push(res.transactionBase64);
57090
+ }
57091
+ return { content: [{ type: "text", text: lines.join(`
57092
+ `) }] };
57093
+ });
57094
+ }
57095
+
56883
57096
  // src/tools/index.ts
56884
57097
  function registerReadTools(server, client) {
56885
57098
  registerHealthTools(server, client);
@@ -56896,6 +57109,7 @@ function registerTransactionTools(server, client) {
56896
57109
  registerClosePositionTool(server, client);
56897
57110
  registerCollateralTools(server, client);
56898
57111
  registerReversePositionTool(server, client);
57112
+ registerTriggerOrderTools(server, client);
56899
57113
  registerSignAndSendTool(server);
56900
57114
  }
56901
57115
 
@@ -56960,11 +57174,11 @@ function registerResources(server, client) {
56960
57174
 
56961
57175
  // src/index.ts
56962
57176
  process.on("uncaughtException", (err) => {
56963
- console.error("[flash-trade-mcp] Uncaught exception:", err);
57177
+ console.error("[flash-trade-mcp] Uncaught exception:", sanitizeError(err));
56964
57178
  process.exit(1);
56965
57179
  });
56966
57180
  process.on("unhandledRejection", (reason) => {
56967
- console.error("[flash-trade-mcp] Unhandled rejection:", reason);
57181
+ console.error("[flash-trade-mcp] Unhandled rejection:", sanitizeError(reason));
56968
57182
  process.exit(1);
56969
57183
  });
56970
57184
  try {
@@ -56972,7 +57186,7 @@ try {
56972
57186
  const client = new FlashApiClient(config2);
56973
57187
  const server = new McpServer({
56974
57188
  name: "flash-trade",
56975
- version: "0.2.2"
57189
+ version: "0.3.2"
56976
57190
  }, {
56977
57191
  capabilities: {
56978
57192
  tools: {},
@@ -56986,6 +57200,6 @@ try {
56986
57200
  const transport = new StdioServerTransport;
56987
57201
  await server.connect(transport);
56988
57202
  } catch (err) {
56989
- console.error("[flash-trade-mcp] Fatal startup error:", err);
57203
+ console.error("[flash-trade-mcp] Fatal startup error:", sanitizeError(err));
56990
57204
  process.exit(1);
56991
57205
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flash-trade-mcp",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "MCP server for Flash Trade — perpetual futures DEX on Solana. Provides AI agents with typed tools for trading, position management, and market data.",
5
5
  "module": "src/index.ts",
6
6
  "main": "dist/index.js",