kinetic-mcp 1.2.0 → 1.2.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/scripts/setup.mjs CHANGED
@@ -1,42 +1,100 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * Interactive setup wizard for Solana Trading MCP Server.
4
+ * Interactive setup wizard for Kinetic MCP Server.
5
5
  *
6
- * Run: npm run setup
6
+ * Run: npx kinetic-mcp setup
7
7
  *
8
8
  * This is a standalone .mjs file — no compilation required.
9
- * Uses only Node built-ins + @solana/web3.js (already installed) + bs58 (transitive dep).
9
+ * Uses @clack/prompts for beautiful terminal UI.
10
10
  */
11
11
 
12
- import { createInterface } from "node:readline/promises";
13
- import { readFileSync, writeFileSync, existsSync, mkdirSync, chmodSync, copyFileSync } from "node:fs";
12
+ import {
13
+ readFileSync,
14
+ writeFileSync,
15
+ existsSync,
16
+ mkdirSync,
17
+ chmodSync,
18
+ copyFileSync,
19
+ } from "node:fs";
14
20
  import { homedir } from "node:os";
15
21
  import { join, resolve, dirname } from "node:path";
16
22
  import { execSync } from "node:child_process";
17
- import { stdin, stdout, platform, execPath } from "node:process";
23
+ import { platform, execPath } from "node:process";
24
+ import { createRequire } from "node:module";
25
+
18
26
  import { Keypair } from "@solana/web3.js";
19
27
  import bs58 from "bs58";
28
+ import * as p from "@clack/prompts";
29
+ import pc from "picocolors";
20
30
 
21
31
  // ---------------------------------------------------------------------------
22
- // Helpers
32
+ // Version
33
+ // ---------------------------------------------------------------------------
34
+
35
+ const require = createRequire(import.meta.url);
36
+ const { version } = require("../package.json");
37
+
38
+ // ---------------------------------------------------------------------------
39
+ // Non-TTY check
23
40
  // ---------------------------------------------------------------------------
24
41
 
25
- const MAX_RETRIES = 3;
42
+ if (!process.stdin.isTTY) {
43
+ console.error("Error: Setup wizard requires an interactive terminal.");
44
+ console.error("Run: npx kinetic-mcp setup");
45
+ process.exit(1);
46
+ }
26
47
 
27
- const rl = createInterface({ input: stdin, output: stdout });
48
+ // ---------------------------------------------------------------------------
49
+ // ASCII Banner
50
+ // ---------------------------------------------------------------------------
28
51
 
29
- function expandHome(p) {
30
- if (p.startsWith("~/") || p.startsWith("~\\")) {
31
- return resolve(homedir(), p.slice(2));
52
+ const BANNER = `
53
+ ██╗ ██╗██╗███╗ ██╗███████╗████████╗██╗ ██████╗
54
+ ██║ ██╔╝██║████╗ ██║██╔════╝╚══██╔══╝██║██╔════╝
55
+ █████╔╝ ██║██╔██╗ ██║█████╗ ██║ ██║██║
56
+ ██╔═██╗ ██║██║╚██╗██║██╔══╝ ██║ ██║██║
57
+ ██║ ██╗██║██║ ╚████║███████╗ ██║ ██║╚██████╗
58
+ ╚═╝ ╚═╝╚═╝╚═╝ ╚═══╝╚══════╝ ╚═╝ ╚═╝ ╚═════╝`;
59
+
60
+ async function renderBanner() {
61
+ let rendered;
62
+ try {
63
+ const gradient = (await import("gradient-string")).default;
64
+ const kineticGradient = gradient(["#FEDA75", "#FA7E1E", "#D62976", "#962FBF", "#4F5BD5"]);
65
+ rendered = kineticGradient.multiline(BANNER);
66
+ } catch {
67
+ rendered = pc.bold(pc.magenta(BANNER));
32
68
  }
33
- return p;
69
+
70
+ console.log(rendered);
71
+ console.log(pc.dim(` v${version} — Solana Trading via Claude Desktop`));
72
+ console.log();
73
+ }
74
+
75
+ // ---------------------------------------------------------------------------
76
+ // Helpers
77
+ // ---------------------------------------------------------------------------
78
+
79
+ function handleCancel(value) {
80
+ if (p.isCancel(value)) {
81
+ p.cancel("Setup cancelled.");
82
+ process.exit(0);
83
+ }
84
+ return value;
85
+ }
86
+
87
+ function expandHome(filePath) {
88
+ if (filePath.startsWith("~/") || filePath.startsWith("~\\")) {
89
+ return resolve(homedir(), filePath.slice(2));
90
+ }
91
+ return filePath;
34
92
  }
35
93
 
36
94
  function isWSL() {
37
95
  try {
38
- const version = readFileSync("/proc/version", "utf-8");
39
- return /microsoft|wsl/i.test(version);
96
+ const ver = readFileSync("/proc/version", "utf-8");
97
+ return /microsoft|wsl/i.test(ver);
40
98
  } catch {
41
99
  return false;
42
100
  }
@@ -48,11 +106,13 @@ function getClaudeConfigPath() {
48
106
  case "darwin":
49
107
  return join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
50
108
  case "win32":
51
- return join(process.env.APPDATA || join(home, "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
109
+ return join(
110
+ process.env.APPDATA || join(home, "AppData", "Roaming"),
111
+ "Claude",
112
+ "claude_desktop_config.json",
113
+ );
52
114
  case "linux":
53
- if (isWSL()) {
54
- return null; // WSL handled separately
55
- }
115
+ if (isWSL()) return null;
56
116
  return join(home, ".config", "Claude", "claude_desktop_config.json");
57
117
  default:
58
118
  return null;
@@ -63,81 +123,6 @@ function getDefaultKeypairPath() {
63
123
  return join(homedir(), ".config", "solana", "trading-keypair.json");
64
124
  }
65
125
 
66
- /** Read a line with masked input (asterisks). Falls back to plain input on non-TTY. */
67
- function readMasked(prompt) {
68
- return new Promise((resolve, reject) => {
69
- stdout.write(prompt);
70
-
71
- if (!stdin.isTTY) {
72
- // Non-interactive: read without masking
73
- const lineRl = createInterface({ input: stdin });
74
- lineRl.on("line", (line) => {
75
- lineRl.close();
76
- resolve(line.trim());
77
- });
78
- return;
79
- }
80
-
81
- stdin.setRawMode(true);
82
- stdin.resume();
83
- stdin.setEncoding("utf8");
84
- let input = "";
85
-
86
- const onData = (ch) => {
87
- if (ch === "\r" || ch === "\n") {
88
- stdin.setRawMode(false);
89
- stdin.pause();
90
- stdin.removeListener("data", onData);
91
- stdout.write("\n");
92
- resolve(input);
93
- } else if (ch === "\u0003") {
94
- // Ctrl+C
95
- stdin.setRawMode(false);
96
- stdin.pause();
97
- stdin.removeListener("data", onData);
98
- stdout.write("\n");
99
- reject(new Error("Cancelled by user"));
100
- } else if (ch === "\u007f" || ch === "\b") {
101
- // Backspace
102
- if (input.length > 0) {
103
- input = input.slice(0, -1);
104
- stdout.write("\b \b");
105
- }
106
- } else if (ch.charCodeAt(0) >= 32) {
107
- // Printable character
108
- input += ch;
109
- stdout.write("*");
110
- }
111
- };
112
-
113
- stdin.on("data", onData);
114
- });
115
- }
116
-
117
- async function askYesNo(question, defaultYes = true) {
118
- const suffix = defaultYes ? "(Y/n)" : "(y/N)";
119
- const answer = await rl.question(`${question} ${suffix} `);
120
- const trimmed = answer.trim().toLowerCase();
121
- if (trimmed === "") return defaultYes;
122
- return trimmed === "y" || trimmed === "yes";
123
- }
124
-
125
- async function askChoice(question, choices) {
126
- console.log(`\n${question}`);
127
- choices.forEach((c, i) => console.log(` ${i + 1}. ${c}`));
128
- for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
129
- const answer = await rl.question(`Choice (1-${choices.length}): `);
130
- const num = parseInt(answer.trim(), 10);
131
- if (num >= 1 && num <= choices.length) return num;
132
- console.log(" Invalid choice. Try again.");
133
- }
134
- throw new Error("Too many invalid attempts.");
135
- }
136
-
137
- // ---------------------------------------------------------------------------
138
- // Keypair handling
139
- // ---------------------------------------------------------------------------
140
-
141
126
  function validateKeypairFile(filePath) {
142
127
  const expanded = expandHome(filePath);
143
128
  if (!existsSync(expanded)) {
@@ -148,112 +133,304 @@ function validateKeypairFile(filePath) {
148
133
  try {
149
134
  parsed = JSON.parse(raw);
150
135
  } catch {
151
- throw new Error("File is not valid JSON. Expected a JSON array of 64 integers (Solana CLI format).");
136
+ throw new Error(
137
+ "File is not valid JSON. Expected a JSON array of 64 integers (Solana CLI format).",
138
+ );
152
139
  }
153
140
  if (
154
141
  !Array.isArray(parsed) ||
155
142
  parsed.length !== 64 ||
156
143
  !parsed.every((n) => typeof n === "number" && Number.isInteger(n))
157
144
  ) {
158
- throw new Error("File does not contain a valid Solana keypair. Expected a JSON array of exactly 64 integers.");
145
+ throw new Error("Invalid Solana keypair. Expected a JSON array of exactly 64 integers.");
159
146
  }
160
147
  return Keypair.fromSecretKey(Uint8Array.from(parsed));
161
148
  }
162
149
 
163
- async function importFromPhantom() {
164
- console.log("\n--- Import Private Key from Phantom ---");
165
- console.log("");
166
- console.log(" How to export from Phantom:");
167
- console.log(" 1. Open Phantom > Settings > Security & Privacy > Export Private Key");
168
- console.log(" 2. Enter your password and copy the base58 string");
169
- console.log("");
170
- console.log(" SECURITY: Your key will be masked. It is NOT saved to shell history.");
171
- console.log("");
172
-
173
- for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
174
- try {
175
- const key = await readMasked(" Paste your private key: ");
176
- if (!key || key.trim().length === 0) {
177
- console.log(" No key entered. Try again.");
178
- continue;
179
- }
150
+ // ---------------------------------------------------------------------------
151
+ // Step 2: Build Check
152
+ // ---------------------------------------------------------------------------
180
153
 
181
- let decoded;
182
- try {
183
- decoded = bs58.decode(key.trim());
184
- } catch {
185
- console.log(" Invalid private key format. Expected a base58-encoded string from Phantom.");
186
- continue;
187
- }
154
+ async function checkBuild() {
155
+ const buildExists = existsSync(resolve("build", "index.js"));
156
+ if (buildExists) return;
188
157
 
189
- if (decoded.length !== 64) {
190
- console.log(` Expected 64-byte secret key, got ${decoded.length} bytes.`);
191
- if (decoded.length === 32) {
192
- console.log(" This looks like a 32-byte seed. Use the Solana CLI to generate the full keypair.");
193
- }
194
- continue;
195
- }
158
+ p.log.warn("Project hasn't been built yet — build/index.js not found.");
196
159
 
197
- const keypair = Keypair.fromSecretKey(decoded);
198
- console.log(`\n Wallet address: ${keypair.publicKey.toBase58()}`);
199
- console.log(" Verify this matches your Phantom wallet address.");
160
+ const runBuild = handleCancel(
161
+ await p.confirm({ message: "Run npm run build now?", initialValue: true }),
162
+ );
200
163
 
201
- // Save keypair file
202
- const defaultPath = getDefaultKeypairPath();
203
- const answer = await rl.question(`\n Save keypair to (${defaultPath}): `);
204
- const savePath = expandHome(answer.trim() || defaultPath);
164
+ if (runBuild) {
165
+ const s = p.spinner();
166
+ s.start("Building project...");
167
+ try {
168
+ execSync("npm run build", { stdio: "pipe" });
169
+ s.stop("Build complete");
170
+ } catch {
171
+ s.stop("Build failed");
172
+ p.log.warn("You can try again later with: npm run build");
173
+ }
174
+ } else {
175
+ p.log.info("Skipped. Remember to run npm run build before using the server.");
176
+ }
177
+ }
205
178
 
206
- mkdirSync(dirname(savePath), { recursive: true });
207
- writeFileSync(savePath, JSON.stringify(Array.from(keypair.secretKey)) + "\n");
179
+ // ---------------------------------------------------------------------------
180
+ // Step 3: Keypair Setup
181
+ // ---------------------------------------------------------------------------
208
182
 
209
- // Set permissions on Unix
210
- if (platform !== "win32") {
183
+ async function importFromPhantom() {
184
+ p.note(
185
+ [
186
+ `${pc.bold("How to export from Phantom:")}`,
187
+ "",
188
+ "1. Open Phantom → Settings → Manage Accounts → [Your Account] → Show Private Key",
189
+ "2. Enter your password and copy the base58 string",
190
+ "",
191
+ `${pc.dim("Your key will be masked. It is NOT saved to shell history.")}`,
192
+ ].join("\n"),
193
+ "Import Private Key",
194
+ );
195
+
196
+ const key = handleCancel(
197
+ await p.password({
198
+ message: "Paste your private key:",
199
+ mask: "*",
200
+ validate(value) {
201
+ if (!value || value.trim().length === 0) return "Private key is required.";
202
+
203
+ let decoded;
211
204
  try {
212
- chmodSync(savePath, 0o600);
205
+ decoded = bs58.decode(value.trim());
213
206
  } catch {
214
- // Non-fatal permissions may not be settable on all filesystems
207
+ return "Invalid format. Expected a base58-encoded string from Phantom.";
215
208
  }
216
- }
217
209
 
218
- console.log(` Keypair saved to: ${savePath}`);
219
- return { keypair, path: savePath };
220
- } catch (err) {
221
- if (err.message === "Cancelled by user") throw err;
222
- console.log(` Error: ${err.message}`);
210
+ if (decoded.length !== 64) {
211
+ if (decoded.length === 32) {
212
+ return "This looks like a 32-byte seed. Use the Solana CLI to generate the full keypair.";
213
+ }
214
+ return `Expected 64-byte secret key, got ${decoded.length} bytes.`;
215
+ }
216
+ },
217
+ }),
218
+ );
219
+
220
+ const decoded = bs58.decode(key.trim());
221
+ const keypair = Keypair.fromSecretKey(decoded);
222
+
223
+ const publicKey = keypair.publicKey.toBase58();
224
+ p.log.info(`Wallet address: ${pc.cyan(publicKey)}`);
225
+ p.log.message(pc.dim("Verify this matches your Phantom wallet address."));
226
+
227
+ // Save keypair to default path
228
+ const defaultPath = getDefaultKeypairPath();
229
+
230
+ // Check if file exists
231
+ if (existsSync(defaultPath)) {
232
+ const overwrite = handleCancel(
233
+ await p.confirm({
234
+ message: `Keypair file already exists at ${pc.dim(defaultPath)}. Overwrite?`,
235
+ initialValue: false,
236
+ }),
237
+ );
238
+
239
+ if (!overwrite) {
240
+ p.log.info("Keeping existing keypair file.");
241
+ return { publicKey, path: defaultPath };
223
242
  }
224
243
  }
225
- throw new Error("Too many invalid attempts. Exiting.");
226
- }
227
244
 
228
- async function useExistingKeypair() {
229
- console.log("\n--- Use Existing Keypair File ---");
230
- console.log(" Expected format: JSON array of 64 integers (Solana CLI format).");
231
- console.log("");
232
-
233
- for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
234
- const filePath = await rl.question(" Path to keypair file: ");
235
- if (!filePath.trim()) {
236
- console.log(" No path entered. Try again.");
237
- continue;
238
- }
245
+ mkdirSync(dirname(defaultPath), { recursive: true, mode: 0o700 });
246
+ writeFileSync(defaultPath, JSON.stringify(Array.from(keypair.secretKey)) + "\n", { mode: 0o600 });
247
+
248
+ if (platform !== "win32") {
239
249
  try {
240
- const expanded = expandHome(filePath.trim());
241
- const keypair = validateKeypairFile(expanded);
242
- console.log(`\n Wallet address: ${keypair.publicKey.toBase58()}`);
243
- return { keypair, path: expanded };
250
+ chmodSync(defaultPath, 0o600);
244
251
  } catch (err) {
245
- console.log(` ${err.message}`);
246
- if (attempt < MAX_RETRIES - 1) console.log(" Try again.");
252
+ p.log.warn(`Could not set file permissions: ${err.message}. Run: chmod 600 ${defaultPath}`);
247
253
  }
248
254
  }
249
- throw new Error("Too many invalid attempts. Exiting.");
255
+
256
+ p.log.success(`Keypair saved to ${pc.dim(defaultPath)}`);
257
+ return { publicKey, path: defaultPath };
258
+ }
259
+
260
+ async function useExistingKeypair() {
261
+ const filePath = handleCancel(
262
+ await p.text({
263
+ message: "Path to keypair file:",
264
+ placeholder: "~/.config/solana/id.json",
265
+ validate(value) {
266
+ if (!value || value.trim().length === 0) return "Path is required.";
267
+ try {
268
+ const expanded = expandHome(value.trim());
269
+ validateKeypairFile(expanded);
270
+ } catch (err) {
271
+ return err.message;
272
+ }
273
+ },
274
+ }),
275
+ );
276
+
277
+ const expanded = expandHome(filePath.trim());
278
+ const keypair = validateKeypairFile(expanded);
279
+ const publicKey = keypair.publicKey.toBase58();
280
+
281
+ p.log.info(`Wallet address: ${pc.cyan(publicKey)}`);
282
+
283
+ return { publicKey, path: expanded };
284
+ }
285
+
286
+ async function setupKeypair() {
287
+ const method = handleCancel(
288
+ await p.select({
289
+ message: "How would you like to set up your Solana keypair?",
290
+ options: [
291
+ {
292
+ value: "phantom",
293
+ label: "Import from Phantom",
294
+ hint: "paste your base58 private key",
295
+ },
296
+ {
297
+ value: "existing",
298
+ label: "Use existing keypair file",
299
+ hint: "Solana CLI JSON format",
300
+ },
301
+ ],
302
+ }),
303
+ );
304
+
305
+ if (method === "phantom") {
306
+ return importFromPhantom();
307
+ } else {
308
+ return useExistingKeypair();
309
+ }
310
+ }
311
+
312
+ // ---------------------------------------------------------------------------
313
+ // Step 4: API Key
314
+ // ---------------------------------------------------------------------------
315
+
316
+ async function setupApiKey() {
317
+ const apiKey = handleCancel(
318
+ await p.password({
319
+ message: "Kinetic API key (optional) (press Enter to skip):",
320
+ mask: "*",
321
+ }),
322
+ );
323
+
324
+ return apiKey?.trim() || null;
250
325
  }
251
326
 
252
327
  // ---------------------------------------------------------------------------
253
- // Claude Desktop config
328
+ // Step 5: Advanced Settings
254
329
  // ---------------------------------------------------------------------------
255
330
 
256
- function generateConfigEntry(keypairPath, apiKey) {
331
+ const DEFAULTS = {
332
+ rpcUrl: "https://api.mainnet-beta.solana.com",
333
+ maxTradeSol: "10",
334
+ priorityFeeLamports: "400000",
335
+ jitoEnabled: false,
336
+ dynamicSlippageMaxBps: "300",
337
+ };
338
+
339
+ async function advancedSettings() {
340
+ const configure = handleCancel(
341
+ await p.confirm({
342
+ message: "Configure advanced settings?",
343
+ initialValue: false,
344
+ }),
345
+ );
346
+
347
+ if (!configure) return {};
348
+
349
+ p.log.info(pc.dim("Press Enter to keep defaults shown."));
350
+
351
+ const rpcUrl = handleCancel(
352
+ await p.text({
353
+ message: "Solana RPC URL:",
354
+ placeholder: DEFAULTS.rpcUrl,
355
+ defaultValue: DEFAULTS.rpcUrl,
356
+ validate(value) {
357
+ if (!value) return;
358
+ try {
359
+ const url = new URL(value);
360
+ if (url.protocol !== "https:") return "URL must use https://";
361
+ } catch {
362
+ return "Invalid URL format.";
363
+ }
364
+ },
365
+ }),
366
+ );
367
+
368
+ const maxTradeSol = handleCancel(
369
+ await p.text({
370
+ message: "Max trade size (SOL):",
371
+ placeholder: DEFAULTS.maxTradeSol,
372
+ defaultValue: DEFAULTS.maxTradeSol,
373
+ validate(value) {
374
+ if (!value) return;
375
+ const num = Number(value);
376
+ if (isNaN(num) || num <= 0) return "Must be a positive number.";
377
+ },
378
+ }),
379
+ );
380
+
381
+ const priorityFeeLamports = handleCancel(
382
+ await p.text({
383
+ message: `Priority fee (lamports): ${pc.dim("400000 = ~0.0004 SOL")}`,
384
+ placeholder: DEFAULTS.priorityFeeLamports,
385
+ defaultValue: DEFAULTS.priorityFeeLamports,
386
+ validate(value) {
387
+ if (!value) return;
388
+ const num = Number(value);
389
+ if (isNaN(num) || !Number.isInteger(num) || num < 0)
390
+ return "Must be a non-negative integer.";
391
+ },
392
+ }),
393
+ );
394
+
395
+ const jitoEnabled = handleCancel(
396
+ await p.confirm({
397
+ message: "Enable Jito MEV protection?",
398
+ initialValue: DEFAULTS.jitoEnabled,
399
+ }),
400
+ );
401
+
402
+ const dynamicSlippageMaxBps = handleCancel(
403
+ await p.text({
404
+ message: "Max dynamic slippage (BPS):",
405
+ placeholder: DEFAULTS.dynamicSlippageMaxBps,
406
+ defaultValue: DEFAULTS.dynamicSlippageMaxBps,
407
+ validate(value) {
408
+ if (!value) return;
409
+ const num = Number(value);
410
+ if (isNaN(num) || !Number.isInteger(num) || num <= 0) return "Must be a positive integer.";
411
+ if (num > 1000) return "Cannot exceed 1000 BPS (10%).";
412
+ },
413
+ }),
414
+ );
415
+
416
+ // Return only non-default values
417
+ const settings = {};
418
+ if (rpcUrl !== DEFAULTS.rpcUrl) settings.SOLANA_RPC_URL = rpcUrl;
419
+ if (maxTradeSol !== DEFAULTS.maxTradeSol) settings.MAX_TRADE_SOL = maxTradeSol;
420
+ if (priorityFeeLamports !== DEFAULTS.priorityFeeLamports)
421
+ settings.PRIORITY_FEE_LAMPORTS = priorityFeeLamports;
422
+ if (jitoEnabled !== DEFAULTS.jitoEnabled) settings.JITO_ENABLED = jitoEnabled ? "true" : "false";
423
+ if (dynamicSlippageMaxBps !== DEFAULTS.dynamicSlippageMaxBps)
424
+ settings.DYNAMIC_SLIPPAGE_MAX_BPS = dynamicSlippageMaxBps;
425
+
426
+ return settings;
427
+ }
428
+
429
+ // ---------------------------------------------------------------------------
430
+ // Step 6: Claude Desktop Config
431
+ // ---------------------------------------------------------------------------
432
+
433
+ function generateConfigEntry(keypairPath, apiKey, advancedEnv) {
257
434
  const projectRoot = resolve(".");
258
435
  const entry = {
259
436
  command: execPath,
@@ -265,55 +442,84 @@ function generateConfigEntry(keypairPath, apiKey) {
265
442
  if (apiKey) {
266
443
  entry.env.API_KEY = apiKey;
267
444
  }
445
+ // Add non-default advanced settings
446
+ if (advancedEnv) {
447
+ Object.assign(entry.env, advancedEnv);
448
+ }
268
449
  return entry;
269
450
  }
270
451
 
271
- function printConfigSnippet(entry) {
452
+ function formatConfigSnippet(entry) {
272
453
  const snippet = {
273
454
  mcpServers: {
274
455
  "solana-trading": entry,
275
456
  },
276
457
  };
277
- console.log("\n Add this to your claude_desktop_config.json:");
278
- console.log("");
279
- console.log(" " + JSON.stringify(snippet, null, 2).split("\n").join("\n "));
280
- console.log("");
281
- console.log(" If you already have other MCP servers configured,");
282
- console.log(' merge the "solana-trading" entry into your existing "mcpServers" object.');
458
+ return JSON.stringify(snippet, null, 2);
283
459
  }
284
460
 
285
- async function configureClaudeDesktop(keypairPath, apiKey) {
286
- console.log("\n--- Configure Claude Desktop ---");
461
+ async function configureClaudeDesktop(keypairPath, apiKey, advancedEnv) {
462
+ const entry = generateConfigEntry(keypairPath, apiKey, advancedEnv);
287
463
 
288
464
  if (isWSL()) {
289
- console.log("");
290
- console.log(" WSL detected. Claude Desktop runs on the Windows host.");
291
- console.log(" Config file is at: %APPDATA%\\Claude\\claude_desktop_config.json");
292
- console.log(" (Usually C:\\Users\\<username>\\AppData\\Roaming\\Claude\\claude_desktop_config.json)");
293
- console.log("");
294
- const entry = generateConfigEntry(keypairPath, apiKey);
295
- printConfigSnippet(entry);
465
+ p.note(
466
+ [
467
+ "WSL detected. Claude Desktop runs on the Windows host.",
468
+ "",
469
+ `Config file: ${pc.dim("%APPDATA%\\Claude\\claude_desktop_config.json")}`,
470
+ "",
471
+ "Add this to your config:",
472
+ "",
473
+ formatConfigSnippet(entry),
474
+ "",
475
+ pc.dim('Merge "solana-trading" into your existing "mcpServers" object.'),
476
+ ].join("\n"),
477
+ "Claude Desktop Config",
478
+ );
296
479
  return;
297
480
  }
298
481
 
299
482
  const configPath = getClaudeConfigPath();
300
483
  if (!configPath) {
301
- console.log(" Could not detect Claude Desktop config path for this platform.");
302
- const entry = generateConfigEntry(keypairPath, apiKey);
303
- printConfigSnippet(entry);
484
+ p.note(
485
+ [
486
+ "Could not detect Claude Desktop config path.",
487
+ "",
488
+ "Add this to your claude_desktop_config.json:",
489
+ "",
490
+ formatConfigSnippet(entry),
491
+ ].join("\n"),
492
+ "Claude Desktop Config",
493
+ );
304
494
  return;
305
495
  }
306
496
 
307
- console.log(` Config file: ${configPath}`);
308
- const entry = generateConfigEntry(keypairPath, apiKey);
497
+ p.log.info(`Config file: ${pc.dim(configPath)}`);
309
498
 
310
- const autoModify = await askYesNo("\n Automatically add this MCP server to Claude Desktop config?");
499
+ const autoModify = handleCancel(
500
+ await p.confirm({
501
+ message: "Automatically add this MCP server to Claude Desktop config?",
502
+ initialValue: true,
503
+ }),
504
+ );
311
505
 
312
506
  if (!autoModify) {
313
- printConfigSnippet(entry);
507
+ p.note(
508
+ [
509
+ "Add this to your claude_desktop_config.json:",
510
+ "",
511
+ formatConfigSnippet(entry),
512
+ "",
513
+ pc.dim('Merge "solana-trading" into your existing "mcpServers" object.'),
514
+ ].join("\n"),
515
+ "Manual Config",
516
+ );
314
517
  return;
315
518
  }
316
519
 
520
+ const s = p.spinner();
521
+ s.start("Writing Claude Desktop config...");
522
+
317
523
  // Read existing config
318
524
  let config = {};
319
525
  if (existsSync(configPath)) {
@@ -321,117 +527,234 @@ async function configureClaudeDesktop(keypairPath, apiKey) {
321
527
  const raw = readFileSync(configPath, "utf-8");
322
528
  config = JSON.parse(raw);
323
529
  } catch {
324
- console.log(" Existing config file has invalid JSON. Cannot auto-modify.");
325
- printConfigSnippet(entry);
530
+ s.stop("Existing config has invalid JSON");
531
+ p.note(
532
+ [
533
+ "Could not parse existing config file.",
534
+ "",
535
+ "Add this manually to your claude_desktop_config.json:",
536
+ "",
537
+ formatConfigSnippet(entry),
538
+ ].join("\n"),
539
+ "Manual Config",
540
+ );
326
541
  return;
327
542
  }
328
543
 
329
544
  // Backup
330
545
  const backupPath = configPath + ".bak";
331
546
  copyFileSync(configPath, backupPath);
332
- console.log(` Backup created: ${backupPath}`);
547
+ p.log.info(`Backup created: ${pc.dim(backupPath)}`);
333
548
  }
334
549
 
335
- // Merge
336
- if (!config.mcpServers) {
337
- config.mcpServers = {};
338
- }
550
+ // Check for existing entry
551
+ if (config.mcpServers?.["solana-trading"]) {
552
+ s.stop("Existing entry found");
553
+ const overwrite = handleCancel(
554
+ await p.confirm({
555
+ message: "A 'solana-trading' entry already exists. Overwrite?",
556
+ initialValue: false,
557
+ }),
558
+ );
339
559
 
340
- if (config.mcpServers["solana-trading"]) {
341
- const overwrite = await askYesNo(" A 'solana-trading' entry already exists. Overwrite?");
342
560
  if (!overwrite) {
343
- console.log(" Skipped. Existing config preserved.");
561
+ p.log.info("Existing config preserved.");
344
562
  return;
345
563
  }
564
+
565
+ s.start("Writing Claude Desktop config...");
346
566
  }
347
567
 
568
+ // Merge
569
+ if (!config.mcpServers) {
570
+ config.mcpServers = {};
571
+ }
348
572
  config.mcpServers["solana-trading"] = entry;
349
573
 
350
574
  // Write
351
575
  mkdirSync(dirname(configPath), { recursive: true });
352
576
  writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
353
- console.log(" Claude Desktop config updated successfully.");
577
+
578
+ s.stop("Claude Desktop config updated");
354
579
  }
355
580
 
356
581
  // ---------------------------------------------------------------------------
357
- // Main wizard
582
+ // Step 7: Verification
358
583
  // ---------------------------------------------------------------------------
359
584
 
360
- async function main() {
361
- console.log("");
362
- console.log(" Solana Trading MCP Server — Setup Wizard");
363
- console.log(" =========================================");
364
- console.log("");
585
+ const MAX_VERIFICATION_RETRIES = 3;
365
586
 
366
- // Step 1: Check build
367
- const buildExists = existsSync(resolve("build", "index.js"));
368
- if (!buildExists) {
369
- console.log(" The project hasn't been built yet (build/index.js not found).");
370
- const runBuild = await askYesNo(" Run 'npm run build' now?");
371
- if (runBuild) {
372
- console.log(" Building...");
587
+ async function runVerification(keypairPath, rpcUrl, apiKey) {
588
+ for (let attempt = 0; attempt <= MAX_VERIFICATION_RETRIES; attempt++) {
589
+ const s = p.spinner();
590
+ let hasFailure = false;
591
+
592
+ // 1. Load keypair
593
+ s.start("Loading keypair...");
594
+ let keypair;
595
+ try {
596
+ keypair = validateKeypairFile(keypairPath);
597
+ s.stop(`Keypair loaded — ${pc.cyan(keypair.publicKey.toBase58())}`);
598
+ } catch (err) {
599
+ s.stop(`Keypair load failed: ${err.message}`);
600
+ hasFailure = true;
601
+ }
602
+
603
+ // 2. Check SOL balance (only if keypair loaded)
604
+ if (keypair) {
605
+ const effectiveRpc = rpcUrl || DEFAULTS.rpcUrl;
606
+ s.start("Checking SOL balance...");
373
607
  try {
374
- execSync("npm run build", { stdio: "inherit" });
375
- console.log(" Build complete.");
376
- } catch {
377
- console.log(" Build failed. You can try again later with: npm run build");
378
- console.log(" Continuing setup anyway...");
608
+ const response = await fetch(effectiveRpc, {
609
+ method: "POST",
610
+ headers: { "Content-Type": "application/json" },
611
+ body: JSON.stringify({
612
+ jsonrpc: "2.0",
613
+ id: 1,
614
+ method: "getBalance",
615
+ params: [keypair.publicKey.toBase58()],
616
+ }),
617
+ signal: AbortSignal.timeout(10_000),
618
+ });
619
+
620
+ const data = await response.json();
621
+ const lamports = data.result?.value ?? 0;
622
+ const sol = (lamports / 1e9).toFixed(4);
623
+
624
+ if (lamports === 0) {
625
+ s.stop(`Balance: ${pc.yellow("0 SOL")}`);
626
+ p.log.warn("Fund this wallet with SOL before trading.");
627
+ } else {
628
+ s.stop(`Balance: ${pc.green(sol + " SOL")}`);
629
+ }
630
+ } catch (err) {
631
+ const msg =
632
+ err.name === "TimeoutError" || err.name === "AbortError"
633
+ ? "Request timed out"
634
+ : err.message;
635
+ s.stop(`Balance check failed: ${pc.yellow(msg)}`);
636
+ hasFailure = true;
637
+ }
638
+ }
639
+
640
+ // 3. Test Kinetic API (only if API key provided)
641
+ if (apiKey) {
642
+ s.start("Testing Kinetic API...");
643
+ try {
644
+ const response = await fetch("https://auth.kinetic.xyz/v1/token", {
645
+ method: "POST",
646
+ headers: { "Content-Type": "application/json" },
647
+ body: JSON.stringify({ apiKey }),
648
+ signal: AbortSignal.timeout(10_000),
649
+ });
650
+
651
+ if (response.ok) {
652
+ s.stop(`Kinetic API: ${pc.green("connected")}`);
653
+ } else {
654
+ s.stop(`Kinetic API: ${pc.yellow(`${response.status} ${response.statusText}`)}`);
655
+ hasFailure = true;
656
+ }
657
+ } catch (err) {
658
+ const msg =
659
+ err.name === "TimeoutError" || err.name === "AbortError"
660
+ ? "Request timed out"
661
+ : err.message;
662
+ s.stop(`Kinetic API: ${pc.yellow(msg)}`);
663
+ hasFailure = true;
664
+ }
665
+ } else {
666
+ p.log.info(pc.dim("Skipping Kinetic API test (no API key)."));
667
+ }
668
+
669
+ if (!hasFailure) return;
670
+
671
+ // Retry on failure (up to MAX_VERIFICATION_RETRIES)
672
+ if (attempt < MAX_VERIFICATION_RETRIES) {
673
+ const retry = handleCancel(
674
+ await p.confirm({
675
+ message: "Some checks failed. Retry verification?",
676
+ initialValue: true,
677
+ }),
678
+ );
679
+
680
+ if (!retry) {
681
+ p.log.warn("Continuing with warnings. You can verify connectivity later.");
682
+ return;
379
683
  }
380
684
  } else {
381
- console.log(" Skipped. Remember to run 'npm run build' before using the server.");
685
+ p.log.warn("Maximum retries reached. Continuing with warnings.");
686
+ return;
382
687
  }
383
688
  }
689
+ }
384
690
 
385
- // Step 2: Keypair setup
386
- const choice = await askChoice("How would you like to set up your Solana keypair?", [
387
- "Import private key from Phantom (or another wallet)",
388
- "Use an existing Solana CLI keypair file",
389
- ]);
691
+ // ---------------------------------------------------------------------------
692
+ // Main
693
+ // ---------------------------------------------------------------------------
390
694
 
391
- let keypairResult;
392
- if (choice === 1) {
393
- keypairResult = await importFromPhantom();
394
- } else {
395
- keypairResult = await useExistingKeypair();
695
+ async function main() {
696
+ await renderBanner();
697
+ p.intro(pc.bold("Setup Wizard"));
698
+
699
+ // Step 2: Build check
700
+ await checkBuild();
701
+
702
+ // Step 3: Keypair setup
703
+ const keypairResult = await setupKeypair();
704
+
705
+ // Step 4: API key
706
+ const apiKey = await setupApiKey();
707
+
708
+ // Step 5: Advanced settings
709
+ const advancedEnv = await advancedSettings();
710
+
711
+ // Step 6: Claude Desktop config
712
+ await configureClaudeDesktop(keypairResult.path, apiKey, advancedEnv);
713
+
714
+ // Step 7: Verification
715
+ const rpcUrl = advancedEnv.SOLANA_RPC_URL || null;
716
+ await runVerification(keypairResult.path, rpcUrl, apiKey);
717
+
718
+ // Step 8: Outro
719
+ const summaryLines = [
720
+ `Wallet: ${pc.cyan(keypairResult.publicKey)}`,
721
+ `Keypair: ${pc.dim(keypairResult.path)}`,
722
+ ];
723
+ if (apiKey) summaryLines.push(`API Key: ${pc.green("configured")}`);
724
+
725
+ const advancedKeys = Object.keys(advancedEnv);
726
+ if (advancedKeys.length > 0) {
727
+ summaryLines.push("");
728
+ summaryLines.push(pc.bold("Custom settings:"));
729
+ for (const [key, val] of Object.entries(advancedEnv)) {
730
+ summaryLines.push(` ${pc.dim(key)}: ${val}`);
731
+ }
396
732
  }
397
733
 
398
- // Step 3: Optional API key
399
- console.log("\n--- API Key (Optional) ---");
400
- console.log(" A Kinetic API key gives you higher rate limits.");
401
- const apiKeyInput = await rl.question(" API Key (press Enter to skip): ");
402
- const apiKey = apiKeyInput.trim() || null;
403
-
404
- // Step 4: Claude Desktop config
405
- await configureClaudeDesktop(keypairResult.path, apiKey);
406
-
407
- // Step 5: Success summary
408
- console.log("");
409
- console.log(" Setup Complete!");
410
- console.log(" ===============");
411
- console.log(` Wallet: ${keypairResult.keypair.publicKey.toBase58()}`);
412
- console.log(` Keypair: ${keypairResult.path}`);
413
- if (apiKey) console.log(" API Key: configured");
414
- console.log("");
415
- console.log(" Next steps:");
416
- console.log(" 1. Restart Claude Desktop (fully quit and reopen)");
417
- console.log(' 2. Look for the hammer icon in the chat input');
418
- console.log(' 3. Try: "What\'s my SOL balance?"');
419
- console.log("");
420
- console.log(" Safety guardrails:");
421
- console.log(" - Swaps always show a preview first (confirm=false)");
422
- console.log(" - Max trade size: 10 SOL (configurable via MAX_TRADE_SOL)");
423
- console.log(" - Slippage capped at 10% (1000 bps)");
424
- console.log("");
425
-
426
- rl.close();
734
+ p.note(summaryLines.join("\n"), "Setup Complete");
735
+
736
+ p.note(
737
+ [
738
+ `1. ${pc.bold("Restart Claude Desktop")} (fully quit and reopen)`,
739
+ `2. Look for the ${pc.bold("hammer icon")} in the chat input`,
740
+ `3. Try: ${pc.cyan('"What\'s my SOL balance?"')}`,
741
+ "",
742
+ pc.dim("Safety guardrails:"),
743
+ pc.dim(` Swaps always show a preview first`),
744
+ pc.dim(` • Max trade size: ${advancedEnv.MAX_TRADE_SOL || "10"} SOL`),
745
+ pc.dim(` Slippage capped at ${advancedEnv.DYNAMIC_SLIPPAGE_MAX_BPS || "300"} BPS`),
746
+ ].join("\n"),
747
+ "Next Steps",
748
+ );
749
+
750
+ p.outro(pc.green("You're all set! Happy trading."));
427
751
  }
428
752
 
429
753
  main().catch((err) => {
430
754
  if (err.message === "Cancelled by user") {
431
- console.log("\n Setup cancelled.");
755
+ p.cancel("Setup cancelled.");
432
756
  } else {
433
- console.error(`\n Setup failed: ${err.message}`);
757
+ p.log.error(`Setup failed: ${err.message}`);
434
758
  }
435
- rl.close();
436
759
  process.exit(1);
437
760
  });