Sign In

@rigel-protocol/solana-mcp

Package Overview
Dependencies
Maintainers
1
Versions
5
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@rigel-protocol/solana-mcp - npm Package Compare versions

Comparing version
1.2.0
to
1.2.1
+2
-2
dist/index.js

@@ -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) => {

@@ -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;

{
"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",