perp-cli 0.6.1 → 0.6.3

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/README.md CHANGED
@@ -160,24 +160,36 @@ Global flags: `--json`, `--fields <f>`, `--ndjson`, `--dry-run`, `--dex <name>`
160
160
 
161
161
  ## MCP Server
162
162
 
163
+ [![Glama MCP server](https://glama.ai/mcp/servers/hypurrquant/perp-cli/badges/score.svg)](https://glama.ai/mcp/servers/hypurrquant/perp-cli)
164
+
163
165
  perp-cli includes a full-featured MCP server (18 tools, 3 resources, 2 prompts) for Claude Desktop, Cursor, and other MCP clients.
164
166
 
167
+ **No API keys required for market data** — explore prices, orderbooks, funding rates, and arb opportunities without any setup. Add keys only when you want to trade.
168
+
165
169
  ```json
166
170
  {
167
171
  "mcpServers": {
168
172
  "perp-cli": {
169
173
  "command": "npx",
170
- "args": ["-y", "-p", "perp-cli", "perp-mcp"],
171
- "env": {
172
- "HYPERLIQUID_PRIVATE_KEY": "your-evm-key",
173
- "PACIFICA_PRIVATE_KEY": "your-solana-key"
174
- }
174
+ "args": ["-y", "-p", "perp-cli", "perp-mcp"]
175
175
  }
176
176
  }
177
177
  }
178
178
  ```
179
179
 
180
- **Tools:** market data, portfolio, orderbook, funding rates, arb scanning, trade preview/execute (with dry-run safety), funding analysis, PnL analysis, arb comparison.
180
+ Optional: add keys for trading and account data:
181
+ ```json
182
+ {
183
+ "env": {
184
+ "HYPERLIQUID_PRIVATE_KEY": "your-evm-key",
185
+ "PACIFICA_PRIVATE_KEY": "your-solana-key"
186
+ }
187
+ }
188
+ ```
189
+
190
+ **Read-only tools (no keys):** `get_markets`, `get_orderbook`, `get_funding_rates`, `get_prices`, `arb_scan`, `health_check`
191
+
192
+ **Account & trading tools (keys required):** `get_balance`, `get_positions`, `portfolio`, `trade_preview`, `trade_execute`, `trade_close`, `get_funding_analysis`, `get_pnl_analysis`, `get_arb_compare`
181
193
 
182
194
  **Resources:** `market://prices`, `market://funding-rates`, `perp://schema`
183
195
 
@@ -687,9 +687,10 @@ export function registerAccountCommands(program, getAdapter, isJson, getAdapterF
687
687
  .command("funding-history")
688
688
  .alias("funding")
689
689
  .description("Personal funding payment history")
690
- .action(async () => {
690
+ .option("-n, --limit <n>", "Number of records", "200")
691
+ .action(async (opts) => {
691
692
  const adapter = await getAdapter();
692
- const payments = await adapter.getFundingPayments(30);
693
+ const payments = await adapter.getFundingPayments(parseInt(opts.limit));
693
694
  if (isJson())
694
695
  return printJson(jsonOk(payments));
695
696
  if (payments.length === 0) {
@@ -294,24 +294,34 @@ export function registerArbManageCommands(program, getAdapterForExchange, isJson
294
294
  const annualPct = hourlyRate * 8760 * 100;
295
295
  const perpNotional = shortPos.size * shortPos.markPrice;
296
296
  const spotValueUsd = spotAmount * shortPos.markPrice; // approx using perp mark price
297
- // Funding income: spot has 0 funding, perp short receives when rate > 0
298
- let estimatedFundingIncome = 0;
299
- if (holdDurationMs && hourlyRate !== 0) {
300
- const holdHours = holdDurationMs / (1000 * 60 * 60);
301
- // Short position: pays when rate < 0, receives when rate > 0
302
- estimatedFundingIncome = Math.abs(hourlyRate) * perpNotional * holdHours;
303
- if (hourlyRate < 0)
304
- estimatedFundingIncome = -estimatedFundingIncome;
297
+ // Funding income: fetch actual payments from exchange API
298
+ let actualFundingIncome = 0;
299
+ try {
300
+ const perpAdapter = await getAdapterForExchange(shortPos.exchange);
301
+ const payments = await perpAdapter.getFundingPayments(200);
302
+ // Filter from entry time minus 1h buffer (multi-leg entry takes time)
303
+ const entryTime = persisted ? new Date(persisted.entryTime).getTime() - 3600000 : 0;
304
+ for (const p of payments) {
305
+ if (p.symbol.toUpperCase().includes(perpSymbol) && (!entryTime || p.time >= entryTime)) {
306
+ actualFundingIncome += Number(p.payment);
307
+ }
308
+ }
305
309
  }
306
- // Add persisted accumulated funding
307
- if (persisted?.accumulatedFunding) {
308
- estimatedFundingIncome += persisted.accumulatedFunding;
310
+ catch {
311
+ // Fallback to estimate if API fails
312
+ if (holdDurationMs && hourlyRate !== 0) {
313
+ const holdHours = holdDurationMs / (1000 * 60 * 60);
314
+ actualFundingIncome = Math.abs(hourlyRate) * perpNotional * holdHours;
315
+ if (hourlyRate < 0)
316
+ actualFundingIncome = -actualFundingIncome;
317
+ }
309
318
  }
310
- // Fees: spot buy + spot sell + perp short entry + perp buy to close
319
+ const estimatedFundingIncome = actualFundingIncome;
320
+ // Fees: entry only (exit not yet paid)
311
321
  const { getTakerFee } = await import("../../constants.js");
312
322
  const spotFee = getTakerFee(spotEx);
313
323
  const perpFee = getTakerFee(shortPos.exchange);
314
- const totalFees = (spotValueUsd * spotFee * 2) + (perpNotional * perpFee * 2);
324
+ const entryFees = (spotValueUsd * spotFee) + (perpNotional * perpFee);
315
325
  const direction = hourlyRate >= 0 ? "long-spot-short-perp" : "sell-spot-long-perp";
316
326
  const dailyFundingEstimate = Math.abs(hourlyRate) * perpNotional * 24;
317
327
  spotPerpPositions.push({
@@ -334,8 +344,8 @@ export function registerArbManageCommands(program, getAdapterForExchange, isJson
334
344
  holdDuration,
335
345
  holdDurationMs,
336
346
  estimatedFundingIncome,
337
- estimatedFees: totalFees,
338
- netPnl: shortPos.unrealizedPnl + estimatedFundingIncome - totalFees,
347
+ estimatedFees: entryFees,
348
+ netPnl: (spotValueUsd - shortPos.entryPrice * spotAmount) + shortPos.unrealizedPnl + estimatedFundingIncome - entryFees,
339
349
  dailyFundingEstimate,
340
350
  });
341
351
  }
@@ -441,29 +451,50 @@ export function registerArbManageCommands(program, getAdapterForExchange, isJson
441
451
  });
442
452
  console.log(makeTable(["Symbol", "Long", "Short", "Size", "Entry", "Mark", "uPnL", "Funding", "Spread", "Basis", "Hold", "Net PnL"], rows));
443
453
  }
444
- // ── Spot-Perp table ──
454
+ // ── Spot-Perp detailed view ──
445
455
  if (spotPerpPositions.length > 0) {
446
- console.log(chalk.cyan.bold(" Spot-Perp Arb Positions\n"));
447
- const spRows = spotPerpPositions.map(sp => {
448
- const spotLabel = sp.spotToken !== sp.symbol
449
- ? `${sp.spotToken} (${sp.spotExchange})`
450
- : sp.spotExchange;
456
+ for (const sp of spotPerpPositions) {
457
+ const exAbbr = (e) => e === "pacifica" ? "PAC" : e === "hyperliquid" ? "HL" : "LT";
451
458
  const spreadStr = sp.currentSpread !== null ? `${sp.currentSpread.toFixed(1)}%` : "-";
452
- return [
453
- chalk.white.bold(sp.symbol),
454
- chalk.green(`${sp.spotAmount.toFixed(4)} ${sp.spotToken}`),
455
- spotLabel,
456
- chalk.red(sp.perpExchange),
457
- `$${formatUsd(sp.perpNotionalUsd)}`,
458
- `$${sp.perpMarkPrice.toFixed(2)}`,
459
- formatPnl(sp.perpUnrealizedPnl),
460
- chalk.yellow(`$${sp.estimatedFundingIncome.toFixed(4)}`),
461
- spreadStr,
462
- sp.holdDuration ?? "-",
463
- formatPnl(sp.netPnl),
459
+ const spotPriceEach = sp.spotAmount > 0 ? sp.spotValueUsd / sp.spotAmount : 0;
460
+ const spotPnl = sp.spotValueUsd - (sp.perpEntryPrice * sp.spotAmount); // spot value vs entry
461
+ const roi = sp.perpNotionalUsd > 0 ? (sp.netPnl / sp.perpNotionalUsd) * 100 : 0;
462
+ const dailyRoi = sp.holdDurationMs && sp.holdDurationMs > 0 ? roi / (sp.holdDurationMs / 86400000) : 0;
463
+ const annRoi = dailyRoi * 365;
464
+ console.log(chalk.cyan.bold(`\n ${sp.symbol} spot-perp`) + chalk.gray(` (${exAbbr(sp.spotExchange)} ↔ ${exAbbr(sp.perpExchange)})`) + chalk.gray(` ${sp.holdDuration ?? "-"}`));
465
+ console.log("");
466
+ // Leg details
467
+ const legRows = [
468
+ [
469
+ chalk.green("Spot"),
470
+ `${sp.spotAmount.toFixed(4)} ${sp.spotToken}`,
471
+ `$${sp.perpEntryPrice.toFixed(4)}`,
472
+ `$${spotPriceEach.toFixed(4)}`,
473
+ formatPnl(spotPnl),
474
+ ],
475
+ [
476
+ chalk.red("Perp"),
477
+ `${sp.perpSize.toFixed(4)} ${sp.symbol}`,
478
+ `$${sp.perpEntryPrice.toFixed(4)}`,
479
+ `$${sp.perpMarkPrice.toFixed(4)}`,
480
+ formatPnl(sp.perpUnrealizedPnl),
481
+ ],
464
482
  ];
465
- });
466
- console.log(makeTable(["Symbol", "Spot Held", "Spot Ex", "Perp Ex", "Size", "Mark", "uPnL", "Funding", "Spread", "Hold", "Net PnL"], spRows));
483
+ console.log(makeTable(["Leg", "Qty", "Entry", "Current", "PnL"], legRows));
484
+ // Summary
485
+ console.log(chalk.white.bold(" PnL Breakdown:"));
486
+ console.log(` Spot PnL: ${formatPnl(spotPnl)}`);
487
+ console.log(` Perp PnL: ${formatPnl(sp.perpUnrealizedPnl)}`);
488
+ console.log(` Funding Earned: ${chalk.yellow(`$${sp.estimatedFundingIncome.toFixed(4)}`)}`);
489
+ console.log(` Trading Fees: ${chalk.red(`-$${sp.estimatedFees.toFixed(4)}`)}`);
490
+ console.log(` ${chalk.white.bold("Total PnL:")} ${formatPnl(sp.netPnl)}${roi !== 0 ? chalk.gray(` (ROI: ${roi >= 0 ? "+" : ""}${roi.toFixed(2)}%)`) : ""}`);
491
+ console.log("");
492
+ console.log(` Current Spread: ${spreadStr}`);
493
+ console.log(` Daily Income: ${chalk.cyan(`$${sp.dailyFundingEstimate.toFixed(4)}/day`)}`);
494
+ if (annRoi !== 0)
495
+ console.log(` Annualized ROI: ${chalk.cyan(`${annRoi >= 0 ? "+" : ""}${annRoi.toFixed(1)}%`)}`);
496
+ }
497
+ console.log("");
467
498
  }
468
499
  // ── Combined Summary ──
469
500
  const totalUpnl = arbPairs.reduce((s, p) => s + p.unrealizedPnl, 0)
@@ -703,18 +703,21 @@ export class HyperliquidAdapter {
703
703
  fee: String(f.fee ?? "0"),
704
704
  }));
705
705
  }
706
- async getFundingPayments(limit = 30) {
706
+ async getFundingPayments(limit = 200) {
707
707
  this.ensureAddress();
708
- const now = Date.now();
709
- const history = await this.client.info.perpetuals.getUserFunding(this._address, now - 7 * 24 * 60 * 60 * 1000);
710
- return (history ?? []).slice(0, limit).map((h) => {
711
- const delta = (h.delta ?? {});
712
- return {
713
- time: Number(h.time ?? 0),
714
- symbol: String(delta.coin ?? ""),
715
- payment: String(delta.usdc ?? "0"),
716
- };
708
+ // Direct API call — SDK's getUserFunding silently truncates results
709
+ const apiUrl = this._testnet ? "https://api.hyperliquid-testnet.xyz/info" : "https://api.hyperliquid.xyz/info";
710
+ const res = await fetch(apiUrl, {
711
+ method: "POST",
712
+ headers: { "Content-Type": "application/json" },
713
+ body: JSON.stringify({ type: "userFunding", user: this._address, startTime: Date.now() - 7 * 24 * 60 * 60 * 1000 }),
717
714
  });
715
+ const data = await res.json();
716
+ return (data ?? []).slice(0, limit).map((h) => ({
717
+ time: h.time,
718
+ symbol: h.delta.coin + "-PERP",
719
+ payment: h.delta.usdc,
720
+ }));
718
721
  }
719
722
  async getFundingHistory(symbol, limit = 10) {
720
723
  const now = Date.now();
@@ -669,7 +669,7 @@ export class LighterAdapter {
669
669
  fee: String(t.fee ?? "0"),
670
670
  }));
671
671
  }
672
- async getFundingPayments(limit = 30) {
672
+ async getFundingPayments(limit = 200) {
673
673
  if (!this._address)
674
674
  throw new Error("No private key configured — account data unavailable. Run: perp init");
675
675
  const raw = await this._getPositionFundingRaw();
@@ -677,7 +677,7 @@ export class LighterAdapter {
677
677
  return items.slice(0, limit).map((f) => ({
678
678
  time: Number(f.timestamp ?? 0) * 1000,
679
679
  symbol: String(f.symbol ?? ""),
680
- payment: String(f.amount ?? f.payment ?? "0"),
680
+ payment: String(f.change ?? f.amount ?? f.payment ?? "0"),
681
681
  }));
682
682
  }
683
683
  /**
@@ -799,8 +799,10 @@ export class LighterAdapter {
799
799
  return this.restGetAuth("/positionFunding", {
800
800
  account_index: String(this._accountIndex),
801
801
  market_id: String(marketId),
802
+ limit: "100",
803
+ side: "all",
802
804
  }).then((res) => {
803
- const items = (res.funding ?? res.data ?? []);
805
+ const items = (res.position_fundings ?? res.funding ?? res.data ?? []);
804
806
  return items.map((f) => ({ ...f, symbol: f.symbol ?? sym }));
805
807
  });
806
808
  }));
@@ -281,7 +281,7 @@ export class PacificaAdapter {
281
281
  fee: String(t.fee ?? "0"),
282
282
  }));
283
283
  }
284
- async getFundingPayments(limit = 30) {
284
+ async getFundingPayments(limit = 200) {
285
285
  if (!this._hasRealKey)
286
286
  throw new Error("No private key configured — account data unavailable. Run: perp init");
287
287
  const raw = await this.client.getFundingAccountHistory(this.account);
@@ -51,6 +51,71 @@ async function getOrCreateAdapter(exchange) {
51
51
  adapters.set(key, adapter);
52
52
  return adapter;
53
53
  }
54
+ /** Try to get adapter, returns null if no key configured (for read-only fallback) */
55
+ async function tryGetAdapter(exchange) {
56
+ const key = exchange.toLowerCase();
57
+ if (adapters.has(key))
58
+ return adapters.get(key);
59
+ try {
60
+ return await getOrCreateAdapter(exchange);
61
+ }
62
+ catch {
63
+ return null;
64
+ }
65
+ }
66
+ /** Fetch markets using public API (no key needed) */
67
+ async function fetchMarketsPublic(exchange) {
68
+ const ex = exchange.toLowerCase();
69
+ if (ex === "pacifica" || ex === "pac") {
70
+ const data = await fetchPacificaPrices();
71
+ return data.map(d => ({ symbol: d.symbol, markPrice: String(d.mark), indexPrice: String(d.mark), fundingRate: String(d.funding), volume24h: "0", openInterest: "0", maxLeverage: 10 }));
72
+ }
73
+ else if (ex === "hyperliquid" || ex === "hl") {
74
+ const data = await fetchHyperliquidMeta();
75
+ return data.map(d => ({ symbol: d.symbol, markPrice: String(d.markPx), indexPrice: String(d.markPx), fundingRate: String(d.funding), volume24h: "0", openInterest: "0", maxLeverage: 50 }));
76
+ }
77
+ else if (ex === "lighter" || ex === "lt") {
78
+ const data = await fetchLighterOrderBookDetails();
79
+ return data.map(d => ({ symbol: d.symbol, markPrice: String(d.lastTradePrice), indexPrice: String(d.lastTradePrice), fundingRate: "0", volume24h: "0", openInterest: "0", maxLeverage: 20 }));
80
+ }
81
+ throw new Error(`Unknown exchange: ${exchange}. Supported: pacifica, hyperliquid, lighter`);
82
+ }
83
+ /** Fetch orderbook using public API (no key needed) */
84
+ async function fetchOrderbookPublic(exchange, symbol) {
85
+ const ex = exchange.toLowerCase();
86
+ if (ex === "hyperliquid" || ex === "hl") {
87
+ const { HYPERLIQUID_API_URL } = await import("./api/public/urls.js");
88
+ const res = await fetch(HYPERLIQUID_API_URL, {
89
+ method: "POST", headers: { "Content-Type": "application/json" },
90
+ body: JSON.stringify({ type: "l2Book", coin: symbol.toUpperCase() }),
91
+ });
92
+ const data = await res.json();
93
+ return {
94
+ bids: (data.levels?.[0] ?? []).map(l => [l.px, l.sz]),
95
+ asks: (data.levels?.[1] ?? []).map(l => [l.px, l.sz]),
96
+ };
97
+ }
98
+ else if (ex === "lighter" || ex === "lt") {
99
+ const { LIGHTER_API_URL } = await import("./api/public/urls.js");
100
+ const res = await fetch(`${LIGHTER_API_URL}/api/v1/orderBookOrders?market_id=0&limit=20`);
101
+ const data = await res.json();
102
+ return {
103
+ bids: (data.bids ?? []).map(l => [l.price, l.remaining_base_amount ?? l.size ?? "0"]),
104
+ asks: (data.asks ?? []).map(l => [l.price, l.remaining_base_amount ?? l.size ?? "0"]),
105
+ };
106
+ }
107
+ else if (ex === "pacifica" || ex === "pac") {
108
+ const { PACIFICA_API_URL } = await import("./api/public/urls.js");
109
+ const baseUrl = PACIFICA_API_URL.replace("/info/prices", "");
110
+ const res = await fetch(`${baseUrl}/info/l2Book?symbol=${symbol.toUpperCase()}`);
111
+ const data = await res.json();
112
+ return {
113
+ bids: (data.l?.[0] ?? []).map(e => [e.p, e.a]),
114
+ asks: (data.l?.[1] ?? []).map(e => [e.p, e.a]),
115
+ };
116
+ }
117
+ throw new Error(`Unknown exchange: ${exchange}`);
118
+ }
54
119
  // ── JSON envelope helpers ──
55
120
  function ok(data, meta) {
56
121
  return JSON.stringify({ ok: true, data, meta }, null, 2);
@@ -65,24 +130,34 @@ const server = new McpServer({ name: "perp-cli", version: _pkg.version }, { capa
65
130
  // ============================================================
66
131
  // Market Data tools (read-only, no private key needed)
67
132
  // ============================================================
68
- server.tool("get_markets", "Get all available perpetual futures markets on an exchange, including price, funding rate, volume, and max leverage", { exchange: z.string().describe("Exchange name: pacifica, hyperliquid, or lighter") }, async ({ exchange }) => {
133
+ server.tool("get_markets", "Get all available perpetual futures markets on an exchange, including price, funding rate, volume, and max leverage. Works without API keys.", { exchange: z.string().describe("Exchange name: pacifica, hyperliquid, or lighter") }, async ({ exchange }) => {
69
134
  try {
70
- const adapter = await getOrCreateAdapter(exchange);
71
- const markets = await adapter.getMarkets();
72
- return { content: [{ type: "text", text: ok(markets, { exchange, count: markets.length }) }] };
135
+ // Try adapter first (richer data), fall back to public API (no key needed)
136
+ const adapter = await tryGetAdapter(exchange);
137
+ if (adapter) {
138
+ const markets = await adapter.getMarkets();
139
+ return { content: [{ type: "text", text: ok(markets, { exchange, count: markets.length }) }] };
140
+ }
141
+ const markets = await fetchMarketsPublic(exchange);
142
+ return { content: [{ type: "text", text: ok(markets, { exchange, count: markets.length, source: "public" }) }] };
73
143
  }
74
144
  catch (e) {
75
145
  return { content: [{ type: "text", text: err(e instanceof Error ? e.message : String(e), { exchange }) }], isError: true };
76
146
  }
77
147
  });
78
- server.tool("get_orderbook", "Get the order book (bids and asks) for a symbol on an exchange", {
148
+ server.tool("get_orderbook", "Get the order book (bids and asks) for a symbol on an exchange. Works without API keys.", {
79
149
  exchange: z.string().describe("Exchange name: pacifica, hyperliquid, or lighter"),
80
150
  symbol: z.string().describe("Trading pair symbol, e.g. BTC, ETH, SOL"),
81
151
  }, async ({ exchange, symbol }) => {
82
152
  try {
83
- const adapter = await getOrCreateAdapter(exchange);
84
- const book = await adapter.getOrderbook(symbol);
85
- return { content: [{ type: "text", text: ok(book, { exchange, symbol }) }] };
153
+ // Try adapter first (richer data), fall back to public API (no key needed)
154
+ const adapter = await tryGetAdapter(exchange);
155
+ if (adapter) {
156
+ const book = await adapter.getOrderbook(symbol);
157
+ return { content: [{ type: "text", text: ok(book, { exchange, symbol }) }] };
158
+ }
159
+ const book = await fetchOrderbookPublic(exchange, symbol);
160
+ return { content: [{ type: "text", text: ok(book, { exchange, symbol, source: "public" }) }] };
86
161
  }
87
162
  catch (e) {
88
163
  return { content: [{ type: "text", text: err(e instanceof Error ? e.message : String(e), { exchange, symbol }) }], isError: true };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "perp-cli",
3
- "version": "0.6.1",
3
+ "version": "0.6.3",
4
4
  "description": "Multi-DEX Perpetual Futures CLI - Pacifica, Hyperliquid, Lighter",
5
5
  "bin": {
6
6
  "perp": "dist/index.js",
@@ -15,7 +15,7 @@
15
15
  "build": "tsc",
16
16
  "dev": "tsc --watch",
17
17
  "test": "vitest run --exclude '**/integration/**'",
18
- "test:integration": "vitest run '**/*.integration.test.ts'",
18
+ "test:integration": "vitest run -c vitest.integration.config.ts",
19
19
  "test:all": "vitest run",
20
20
  "prepublishOnly": "pnpm run build"
21
21
  },
@@ -40,7 +40,7 @@
40
40
  ],
41
41
  "license": "MIT",
42
42
  "engines": {
43
- "node": "^20.0.0 || ^22.0.0"
43
+ "node": "^20.0.0 || ^22.0.0 || ^24.0.0"
44
44
  },
45
45
  "dependencies": {
46
46
  "@modelcontextprotocol/sdk": "^1.27.1",
@@ -5,12 +5,29 @@ allowed-tools: "Bash(perp:*), Bash(npx perp-cli:*), Bash(npx -y perp-cli:*)"
5
5
  license: MIT
6
6
  metadata:
7
7
  author: hypurrquant
8
- version: "0.5.1"
8
+ version: "0.6.1"
9
9
  ---
10
10
 
11
11
  # perp-cli Agent Guide
12
12
 
13
- Multi-DEX perpetual futures CLI — Pacifica (Solana), Hyperliquid (HyperEVM), Lighter (Ethereum).
13
+ Multi-DEX perpetual futures CLI + MCP server — Pacifica (Solana), Hyperliquid (HyperEVM), Lighter (Ethereum).
14
+
15
+ [![Glama MCP server](https://glama.ai/mcp/servers/hypurrquant/perp-cli/badges/score.svg)](https://glama.ai/mcp/servers/hypurrquant/perp-cli)
16
+
17
+ ## MCP Server
18
+
19
+ 18 tools, 3 resources, 2 prompts. No API keys required for market data — explore prices, orderbooks, funding rates, and arb opportunities without setup.
20
+
21
+ ```json
22
+ {
23
+ "mcpServers": {
24
+ "perp-cli": {
25
+ "command": "npx",
26
+ "args": ["-y", "-p", "perp-cli", "perp-mcp"]
27
+ }
28
+ }
29
+ }
30
+ ```
14
31
 
15
32
  ## Rules
16
33
 
@@ -24,7 +41,7 @@ Multi-DEX perpetual futures CLI — Pacifica (Solana), Hyperliquid (HyperEVM), L
24
41
  ## Install
25
42
 
26
43
  ```bash
27
- perp --version 2>/dev/null # check if exists (must be >= 0.5.1)
44
+ perp --version 2>/dev/null # check if exists (must be >= 0.6.1)
28
45
  npm install -g perp-cli@latest 2>/dev/null || npx -y perp-cli@latest --json --version
29
46
  ```
30
47