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