@rigel-protocol/solana-mcp
Advanced tools
+2
-2
@@ -25,3 +25,3 @@ #!/usr/bin/env node | ||
| name: "rigel-solana-mcp", | ||
| version: "1.0.0", | ||
| version: "1.2.1", | ||
| }); | ||
@@ -97,3 +97,3 @@ /** Wrap a tool handler with uniform error handling. */ | ||
| await server.connect(transport); | ||
| log("rigel-solana-mcp v1.2.0 ready (stdio) — 7 tools registered, read-only."); | ||
| log("rigel-solana-mcp v1.2.1 ready (stdio) — 7 tools registered, read-only."); | ||
| } | ||
@@ -100,0 +100,0 @@ main().catch((err) => { |
+24
-10
@@ -29,18 +29,32 @@ /** | ||
| } | ||
| /** Wrap an RPC call with a clearer error message on connection failures. */ | ||
| const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); | ||
| /** Wrap an RPC call with retry/backoff on rate limits, plus clearer errors. */ | ||
| export async function withRpc(what, fn) { | ||
| try { | ||
| return await fn(); | ||
| } | ||
| catch (err) { | ||
| const msg = err instanceof Error ? err.message : String(err); | ||
| if (/fetch|network|ECONN|timeout|429/i.test(msg)) { | ||
| throw new Error(`RPC request failed while ${what} (${msg}). ` + | ||
| `The public mainnet RPC rate-limits aggressively — consider setting SOLANA_RPC_URL to a free Helius/Triton endpoint.`); | ||
| const MAX_TRIES = 4; | ||
| let lastMsg = ""; | ||
| for (let attempt = 0; attempt < MAX_TRIES; attempt++) { | ||
| try { | ||
| return await fn(); | ||
| } | ||
| throw new Error(`Failed while ${what}: ${msg}`); | ||
| catch (err) { | ||
| lastMsg = err instanceof Error ? err.message : String(err); | ||
| const retryable = /429|rate|timeout|ECONN|fetch failed|socket|network/i.test(lastMsg); | ||
| if (retryable && attempt < MAX_TRIES - 1) { | ||
| await sleep(400 * Math.pow(2, attempt)); // 400ms, 800ms, 1600ms | ||
| continue; | ||
| } | ||
| break; | ||
| } | ||
| } | ||
| if (/429|rate|timeout|ECONN|fetch|network|socket/i.test(lastMsg)) { | ||
| throw new Error(`RPC rate-limited or unreachable while ${what} (after ${MAX_TRIES} tries). ` + | ||
| `The public mainnet RPC throttles hard — set SOLANA_RPC_URL to a free Helius/Triton endpoint for reliable results.`); | ||
| } | ||
| throw new Error(`Failed while ${what}: ${lastMsg}`); | ||
| } | ||
| export const LAMPORTS_PER_SOL = 1_000_000_000; | ||
| export const TOKEN_PROGRAM_ID = new PublicKey("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"); | ||
| export const TOKEN_2022_PROGRAM_ID = new PublicKey("TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"); | ||
| /** Both token programs — mint may be classic SPL or Token-2022. */ | ||
| export const TOKEN_PROGRAMS = [TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID]; | ||
| /** pump.fun program + bonding-curve constants (mainnet). */ | ||
@@ -47,0 +61,0 @@ export const PUMP_PROGRAM_ID = new PublicKey("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"); |
@@ -12,3 +12,3 @@ /** | ||
| import { PublicKey } from "@solana/web3.js"; | ||
| import { conn, parsePubkey, withRpc, fmt, LAMPORTS_PER_SOL, TOKEN_PROGRAM_ID } from "../solana.js"; | ||
| import { conn, parsePubkey, withRpc, fmt, LAMPORTS_PER_SOL, TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID } from "../solana.js"; | ||
| const MAX_PAGES = 4; // × 1000 signatures | ||
@@ -45,12 +45,14 @@ async function oldestSignature(addr) { | ||
| // 2. profile the deployer | ||
| const [walletHist, lamports, tokenAccounts, supplyRes] = await Promise.all([ | ||
| const [walletHist, lamports, tok, tok2022, supplyRes] = await Promise.all([ | ||
| oldestSignature(deployer), | ||
| withRpc("fetching deployer balance", () => conn().getBalance(deployer)), | ||
| withRpc("fetching deployer holdings", () => conn().getParsedTokenAccountsByOwner(deployer, { programId: TOKEN_PROGRAM_ID })), | ||
| withRpc("fetching deployer holdings (t22)", () => conn().getParsedTokenAccountsByOwner(deployer, { programId: TOKEN_2022_PROGRAM_ID }).catch(() => ({ value: [] }))), | ||
| withRpc("fetching supply", () => conn().getTokenSupply(mint).catch(() => null)), | ||
| ]); | ||
| const allTokenAccounts = [...tok.value, ...tok2022.value]; | ||
| const walletAgeDays = walletHist.sig?.blockTime ? days(walletHist.sig.blockTime) : null; | ||
| const launchAgeDays = mintHist.sig.blockTime ? days(mintHist.sig.blockTime) : null; | ||
| const supply = Number(supplyRes?.value?.uiAmount ?? 0); | ||
| const bag = tokenAccounts.value | ||
| const bag = allTokenAccounts | ||
| .map((a) => a.account.data.parsed?.info) | ||
@@ -60,3 +62,3 @@ .find((i) => i?.mint === mint.toBase58()); | ||
| const bagPct = supply > 0 ? (bagAmount / supply) * 100 : null; | ||
| const otherTokens = tokenAccounts.value.filter((a) => Number(a.account.data.parsed?.info?.tokenAmount?.uiAmount ?? 0) > 0).length; | ||
| const otherTokens = allTokenAccounts.filter((a) => Number(a.account.data.parsed?.info?.tokenAmount?.uiAmount ?? 0) > 0).length; | ||
| // verdict | ||
@@ -63,0 +65,0 @@ const flags = []; |
@@ -40,4 +40,9 @@ /** | ||
| const results = await Promise.allSettled(SECTIONS.map(([, fn]) => fn(tokenAddress))); | ||
| // the two checks that actually test for a rug — if either failed, the | ||
| // audit CANNOT claim the token is clean. | ||
| const CORE = new Set(["SAFETY", "BUNDLE CHECK"]); | ||
| let worst = 0; | ||
| let coreFailed = 0; | ||
| const reasons = []; | ||
| const failedNames = []; | ||
| const body = []; | ||
@@ -53,7 +58,20 @@ results.forEach((r, i) => { | ||
| else { | ||
| body.push(`(unavailable: ${r.reason instanceof Error ? r.reason.message : r.reason})`); | ||
| const msg = r.reason instanceof Error ? r.reason.message : String(r.reason); | ||
| body.push(`(unavailable: ${msg})`); | ||
| failedNames.push(name); | ||
| if (CORE.has(name)) | ||
| coreFailed++; | ||
| } | ||
| body.push(""); | ||
| }); | ||
| const verdict = worst >= 2 ? "HIGH RISK" : worst === 1 ? "PROCEED WITH CAUTION" : "NO MAJOR FLAGS"; | ||
| // verdict — a safety tool must never say "clean" when it couldn't check safety | ||
| let verdict; | ||
| if (worst >= 2) | ||
| verdict = "HIGH RISK"; | ||
| else if (coreFailed > 0) | ||
| verdict = "INCONCLUSIVE — core safety checks could not run"; | ||
| else if (worst === 1) | ||
| verdict = "PROCEED WITH CAUTION"; | ||
| else | ||
| verdict = "NO MAJOR FLAGS FOUND"; | ||
| const head = []; | ||
@@ -63,2 +81,6 @@ head.push(`✦ RIGEL FULL AUDIT — ${tokenAddress.trim()}`); | ||
| head.push(`OVERALL: ${verdict}`); | ||
| if (coreFailed > 0) { | ||
| head.push(`⚠ ${failedNames.join(" + ")} unavailable — the checks that catch rugs did not complete.`); | ||
| head.push(` Do not read this as "safe." Retry in a minute, or set SOLANA_RPC_URL to a Helius/Triton key.`); | ||
| } | ||
| if (reasons.length) { | ||
@@ -65,0 +87,0 @@ head.push("Top reasons:"); |
@@ -10,2 +10,15 @@ /** | ||
| import { conn, parsePubkey, withRpc, fmt } from "../solana.js"; | ||
| /** ratio using raw base units (avoids float error on huge supplies). */ | ||
| function rawPct(accounts, n, supplyRaw) { | ||
| if (supplyRaw <= 0n) | ||
| return null; | ||
| let top = 0n; | ||
| for (const a of accounts.slice(0, n)) { | ||
| try { | ||
| top += BigInt(a.amount ?? "0"); | ||
| } | ||
| catch { /* skip */ } | ||
| } | ||
| return Number((top * 10000n) / supplyRaw) / 100; | ||
| } | ||
| export async function analyzeRugRisk(tokenAddress) { | ||
@@ -19,12 +32,15 @@ const mint = parsePubkey(tokenAddress, "token address"); | ||
| const parsed = mintInfo.value?.data; | ||
| if (!parsed || parsed.program !== "spl-token" || parsed.parsed?.type !== "mint") { | ||
| throw new Error(`${mint.toBase58()} is not an SPL token mint account.`); | ||
| // accept classic SPL token AND Token-2022 (modern pump.fun mints) | ||
| const prog = parsed?.program ?? ""; | ||
| const isTokenMint = (prog === "spl-token" || prog === "spl-token-2022") && parsed?.parsed?.type === "mint"; | ||
| if (!isTokenMint) { | ||
| throw new Error(`${mint.toBase58()} is not an SPL token or Token-2022 mint account.`); | ||
| } | ||
| const isToken2022 = prog === "spl-token-2022"; | ||
| const info = parsed.parsed.info; | ||
| const mintAuthority = info.mintAuthority ?? null; | ||
| const freezeAuthority = info.freezeAuthority ?? null; | ||
| const supplyRaw = BigInt(supplyRes?.value?.amount ?? "0"); | ||
| const supply = Number(supplyRes?.value?.uiAmount ?? 0); | ||
| const holders = (largest?.value ?? []).map((h) => Number(h.uiAmount ?? 0)); | ||
| const top10 = holders.slice(0, 10).reduce((s, v) => s + v, 0); | ||
| const top10Pct = supply > 0 ? (top10 / supply) * 100 : null; | ||
| const top10Pct = largest?.value ? rawPct(largest.value, 10, supplyRaw) : null; | ||
| // ---- scoring ---- | ||
@@ -41,2 +57,5 @@ let score = 0; | ||
| } | ||
| if (isToken2022) { | ||
| flags.push("Token-2022 mint — may carry transfer fees or a transfer hook that can tax or block selling. Verify extensions before trusting liquidity."); | ||
| } | ||
| if (top10Pct !== null) { | ||
@@ -62,5 +81,9 @@ if (top10Pct >= 60) { | ||
| lines.push(`${mark(!freezeAuthority)} Freeze authority: ${freezeAuthority ? `ACTIVE (${freezeAuthority})` : "none · safe"}`); | ||
| lines.push(`${isToken2022 ? "⚠" : "✓"} Token program: ${isToken2022 ? "Token-2022 (check extensions)" : "classic SPL"}`); | ||
| if (top10Pct !== null) { | ||
| lines.push(`${mark(top10Pct < 30)} Top-10 holder concentration: ${top10Pct.toFixed(1)}% of ${fmt(supply)} supply`); | ||
| } | ||
| else { | ||
| lines.push(`⚠ Top-10 holder concentration: unavailable (RPC could not return holders)`); | ||
| } | ||
| lines.push(""); | ||
@@ -67,0 +90,0 @@ lines.push(`Risk rating: ${rating}`); |
| /** | ||
| * rigel_get_wallet_balance — SOL balance + major SPL token holdings. | ||
| */ | ||
| import { conn, parsePubkey, withRpc, fmt, LAMPORTS_PER_SOL, TOKEN_PROGRAM_ID, } from "../solana.js"; | ||
| import { conn, parsePubkey, withRpc, fmt, LAMPORTS_PER_SOL, TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID, } from "../solana.js"; | ||
| export async function getWalletBalance(walletAddress) { | ||
| const owner = parsePubkey(walletAddress, "wallet address"); | ||
| const [lamports, tokenAccounts] = await Promise.all([ | ||
| const [lamports, tokenAccounts, t22] = await Promise.all([ | ||
| withRpc("fetching SOL balance", () => conn().getBalance(owner)), | ||
| withRpc("fetching token accounts", () => conn().getParsedTokenAccountsByOwner(owner, { programId: TOKEN_PROGRAM_ID })), | ||
| withRpc("fetching Token-2022 accounts", () => conn().getParsedTokenAccountsByOwner(owner, { programId: TOKEN_2022_PROGRAM_ID }).catch(() => ({ value: [] }))), | ||
| ]); | ||
| const sol = lamports / LAMPORTS_PER_SOL; | ||
| const holdings = tokenAccounts.value | ||
| const holdings = [...tokenAccounts.value, ...t22.value] | ||
| .map((a) => { | ||
@@ -14,0 +15,0 @@ const info = a.account.data.parsed?.info; |
+1
-1
| { | ||
| "name": "@rigel-protocol/solana-mcp", | ||
| "version": "1.2.0", | ||
| "version": "1.2.1", | ||
| "description": "Open-source MCP server that gives AI agents read-only eyes on Solana: wallet balances, pump.fun token data, and rug-risk checks.", | ||
@@ -5,0 +5,0 @@ "license": "MIT", |
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
43454
7.7%813
8.26%