AMOCA Solana MCP Server

by manolaz
Verified
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import * as dotenv from "dotenv"; import { z } from "zod"; import { Connection, PublicKey, LAMPORTS_PER_SOL, clusterApiUrl, Keypair, Transaction } from "@solana/web3.js"; import { getOrCreateAssociatedTokenAccount } from "@solana/spl-token"; import bs58 from "bs58"; dotenv.config(); // Jupiter API constants const JUPITER_QUOTE_API = 'https://quote-api.jup.ag/v6'; // Renamed from SolanaTrader to AMOCASolanaAgent class AMOCASolanaAgent { connection; constructor(connection) { this.connection = connection; } createWallet() { const keypair = Keypair.generate(); return { publicKey: keypair.publicKey.toString(), privateKey: bs58.encode(keypair.secretKey), }; } importWallet(privateKey) { try { const keypair = Keypair.fromSecretKey(bs58.decode(privateKey)); return { publicKey: keypair.publicKey.toString(), privateKey, }; } catch (error) { throw new Error('Invalid private key'); } } async getTokenBalance(walletAddress, tokenMint) { try { const wallet = new PublicKey(walletAddress); const mint = new PublicKey(tokenMint); const tokenAccount = await getOrCreateAssociatedTokenAccount(this.connection, Keypair.generate(), // dummy signer for read-only mint, wallet); const balance = await this.connection.getTokenAccountBalance(tokenAccount.address); return balance.value.uiAmountString || '0'; } catch (error) { console.error('Error getting token balance:', error); throw error; } } async getTokenPrices(mintAddresses) { try { // Use Jupiter Price API to fetch token prices const response = await fetch("https://price.jup.ag/v4/price", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ ids: mintAddresses, }), }); if (!response.ok) { throw new Error("Failed to fetch token prices"); } const data = await response.json(); const prices = {}; // Format the price data for (const mint of mintAddresses) { if (data.data && data.data[mint]) { prices[mint] = { price: data.data[mint].price || 0, symbol: data.data[mint].symbol }; } else { prices[mint] = { price: 0 }; } } return prices; } catch (error) { console.error("Error fetching token prices:", error); // Return empty prices if API fails return mintAddresses.reduce((acc, mint) => { acc[mint] = { price: 0 }; return acc; }, {}); } } async getAllTokenBalances(walletAddress) { try { const wallet = new PublicKey(walletAddress); // Get all token accounts owned by this wallet const tokenAccounts = await this.connection.getTokenAccountsByOwner(wallet, { programId: new PublicKey('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'), // SPL Token program ID }); const balances = {}; const mintAddresses = []; // Process each token account for (const tokenAccount of tokenAccounts.value) { const accountInfo = tokenAccount.account; const accountData = Buffer.from(accountInfo.data); // Extract mint address from token account data (first 32 bytes) const mintAddress = new PublicKey(accountData.slice(0, 32)).toString(); mintAddresses.push(mintAddress); // Get parsed token info for better data presentation const parsedAccountInfo = await this.connection.getParsedAccountInfo(tokenAccount.pubkey); if (parsedAccountInfo.value && 'parsed' in parsedAccountInfo.value.data) { const parsedData = parsedAccountInfo.value.data.parsed; if ('info' in parsedData && 'tokenAmount' in parsedData.info) { const amount = parsedData.info.tokenAmount.uiAmountString || '0'; balances[mintAddress] = amount; } } } // Get token prices const prices = await this.getTokenPrices(mintAddresses); // Calculate USD values for each token const tokenDetails = {}; let totalUsdValue = 0; for (const [mint, amount] of Object.entries(balances)) { const price = prices[mint]?.price || 0; const usdValue = parseFloat(amount) * price; totalUsdValue += usdValue; tokenDetails[mint] = { amount, usdValue, symbol: prices[mint]?.symbol }; } // Create histogram data for value distribution const ranges = ['$0-$1', '$1-$10', '$10-$100', '$100-$1K', '$1K-$10K', '$10K+']; const thresholds = [0, 1, 10, 100, 1000, 10000, Infinity]; const counts = new Array(ranges.length).fill(0); // Count tokens in each value range for (const token of Object.values(tokenDetails)) { for (let i = 0; i < thresholds.length - 1; i++) { if (token.usdValue >= thresholds[i] && token.usdValue < thresholds[i + 1]) { counts[i]++; break; } } } // Create ASCII histogram const maxCount = Math.max(...counts); const histogramWidth = 30; // Max width of histogram bars let distribution = 'Token Value Distribution:\n\n'; for (let i = 0; i < ranges.length; i++) { const barWidth = maxCount > 0 ? Math.round((counts[i] / maxCount) * histogramWidth) : 0; const bar = '█'.repeat(barWidth); distribution += `${ranges[i].padEnd(10)} | ${bar} ${counts[i]}\n`; } return { tokens: tokenDetails, histogram: { ranges, counts, distribution }, totalUsdValue }; } catch (error) { console.error('Error getting all token balances:', error); throw error; } } async getSwapQuote(params) { try { const response = await fetch(`${JUPITER_QUOTE_API}/quote?inputMint=${params.inputMint}&outputMint=${params.outputMint}&amount=${params.amount}&slippageBps=${params.slippage * 100}`, { headers: { 'Content-Type': 'application/json', }, }); if (!response.ok) { throw new Error('Failed to get swap quote'); } return await response.json(); } catch (error) { console.error('Error getting swap quote:', error); throw error; } } async executeSwap(quote, walletPrivateKey) { try { // Get serialized transactions from Jupiter const swapResponse = await fetch(`${JUPITER_QUOTE_API}/swap`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ quoteResponse: quote, userPublicKey: Keypair.fromSecretKey(bs58.decode(walletPrivateKey)).publicKey.toString(), }), }); if (!swapResponse.ok) { throw new Error('Failed to prepare swap transaction'); } const swapData = await swapResponse.json(); const swapTransactionBuf = Buffer.from(swapData.swapTransaction, 'base64'); // Deserialize and sign transaction const transaction = Transaction.from(swapTransactionBuf); const keypair = Keypair.fromSecretKey(bs58.decode(walletPrivateKey)); transaction.sign(keypair); // Send transaction const txid = await this.connection.sendRawTransaction(transaction.serialize(), { skipPreflight: true }); // Wait for confirmation const confirmation = await this.connection.confirmTransaction(txid); if (confirmation.value.err) { throw new Error('Transaction failed'); } return { txid, status: 'confirmed', }; } catch (error) { console.error('Error executing swap:', error); throw error; } } } // Create an MCP server const server = new McpServer({ name: "Solana RPC Tools", version: "1.0.0", }); // Initialize Solana connection const connection = new Connection(clusterApiUrl("mainnet-beta"), "confirmed"); // const connection = new Connection(clusterApiUrl("devnet"), "confirmed"); // Initialize AMOCASolanaAgent with our connection (renamed from trader) const agent = new AMOCASolanaAgent(connection); // Solana RPC Methods as Tools // Get Account Info server.tool("getAccountInfo", "Used to look up account info by public key (32 byte base58 encoded address)", { publicKey: z.string() }, async ({ publicKey }) => { try { const pubkey = new PublicKey(publicKey); const accountInfo = await connection.getAccountInfo(pubkey); return { content: [{ type: "text", text: JSON.stringify(accountInfo, null, 2) }] }; } catch (error) { return { content: [{ type: "text", text: `Error: ${error.message}` }] }; } }); // Get Balance server.tool("getBalance", "Get comprehensive token portfolio for a wallet with visual charts", { publicKey: z.string() }, async ({ publicKey }) => { try { const pubkey = new PublicKey(publicKey); // Get SOL balance const solBalance = await connection.getBalance(pubkey); // Get all token balances with their values const tokenBalances = await agent.getAllTokenBalances(publicKey); // Create a more visual representation with ASCII art let response = `📊 WALLET PORTFOLIO SUMMARY 📊\n\n`; response += `🔷 SOL Balance: ${(solBalance / LAMPORTS_PER_SOL).toFixed(6)} SOL (${solBalance} lamports)\n`; response += `💰 Total Portfolio Value: $${tokenBalances.totalUsdValue.toFixed(2)}\n\n`; // Value distribution histogram response += `📈 TOKEN VALUE DISTRIBUTION 📈\n`; response += tokenBalances.histogram.distribution; // Top tokens table with better formatting response += `\n🏆 TOP TOKENS BY VALUE 🏆\n`; response += `${"TOKEN".padEnd(12)} | ${"AMOUNT".padEnd(18)} | ${"USD VALUE".padEnd(12)}\n`; response += `${"-".repeat(50)}\n`; // Sort tokens by USD value and get top 8 const topTokens = Object.entries(tokenBalances.tokens) .sort((a, b) => b[1].usdValue - a[1].usdValue) .slice(0, 8); for (const [mint, details] of topTokens) { const symbol = details.symbol || "Unknown"; const formattedAmount = details.amount.length > 15 ? `${details.amount.substring(0, 12)}...` : details.amount.padEnd(15); response += `${symbol.padEnd(12)} | ${formattedAmount.padEnd(18)} | $${details.usdValue.toFixed(2).padEnd(12)}\n`; } // Add a pie chart representation using ASCII const totalValue = tokenBalances.totalUsdValue; if (totalValue > 0) { response += `\n🥧 PORTFOLIO COMPOSITION 🥧\n`; for (const [mint, details] of topTokens) { if (details.usdValue > 0) { const percentage = (details.usdValue / totalValue) * 100; const barLength = Math.max(1, Math.round((percentage / 100) * 30)); const symbol = details.symbol || "Unknown"; response += `${symbol.padEnd(10)} | ${"█".repeat(barLength)} ${percentage.toFixed(1)}%\n`; } } } return { content: [{ type: "text", text: response }] }; } catch (error) { return { content: [{ type: "text", text: `Error: ${error.message}` }] }; } }); // Get Minimum Balance For Rent Exemption server.tool("getMinimumBalanceForRentExemption", "Used to look up minimum balance required for rent exemption by data size", { dataSize: z.number() }, async ({ dataSize }) => { try { const minBalance = await connection.getMinimumBalanceForRentExemption(dataSize); return { content: [{ type: "text", text: `${minBalance / LAMPORTS_PER_SOL} SOL (${minBalance} lamports)` }] }; } catch (error) { return { content: [{ type: "text", text: `Error: ${error.message}` }] }; } }); // Get Transaction server.tool("getTransaction", "Used to look up transaction by signature (64 byte base58 encoded string)", { signature: z.string() }, async ({ signature }) => { try { const transaction = await connection.getParsedTransaction(signature, { maxSupportedTransactionVersion: 0 }); return { content: [{ type: "text", text: JSON.stringify(transaction, null, 2) }] }; } catch (error) { return { content: [{ type: "text", text: `Error: ${error.message}` }] }; } }); // New trading tools // Create Wallet server.tool("createWallet", "Create a new Solana wallet keypair", {}, async () => { try { const wallet = agent.createWallet(); return { content: [{ type: "text", text: JSON.stringify(wallet, null, 2) }] }; } catch (error) { return { content: [{ type: "text", text: `Error: ${error.message}` }] }; } }); // Import Wallet server.tool("importWallet", "Import an existing Solana wallet using private key", { privateKey: z.string() }, async ({ privateKey }) => { try { const wallet = agent.importWallet(privateKey); return { content: [{ type: "text", text: JSON.stringify(wallet, null, 2) }] }; } catch (error) { return { content: [{ type: "text", text: `Error: ${error.message}` }] }; } }); // Get Token Balance server.tool("getTokenBalance", "Get token balance for a wallet", { walletAddress: z.string(), tokenMint: z.string() }, async ({ walletAddress, tokenMint }) => { try { const balance = await agent.getTokenBalance(walletAddress, tokenMint); return { content: [{ type: "text", text: balance }] }; } catch (error) { return { content: [{ type: "text", text: `Error: ${error.message}` }] }; } }); // Get All Token Balances server.tool("getAllTokenBalances", "Get all token balances for a wallet with USD value distribution", { walletAddress: z.string() }, async ({ walletAddress }) => { try { const balanceSummary = await agent.getAllTokenBalances(walletAddress); // Format the response for better readability let response = `Wallet: ${walletAddress}\n`; response += `Total Value: $${balanceSummary.totalUsdValue.toFixed(2)}\n\n`; response += balanceSummary.histogram.distribution; response += "\n\nTop 5 tokens by value:\n"; // Sort tokens by USD value and get top 5 const topTokens = Object.entries(balanceSummary.tokens) .sort((a, b) => b[1].usdValue - a[1].usdValue) .slice(0, 5); for (const [mint, details] of topTokens) { const symbol = details.symbol || "Unknown"; response += `${symbol.padEnd(10)} | ${details.amount.padEnd(15)} | $${details.usdValue.toFixed(2)}\n`; } return { content: [{ type: "text", text: response }] }; } catch (error) { return { content: [{ type: "text", text: `Error: ${error.message}` }] }; } }); // Get Swap Quote server.tool("getSwapQuote", "Get a quote for swapping tokens via Jupiter", { inputMint: z.string(), outputMint: z.string(), amount: z.string(), slippage: z.number() }, async ({ inputMint, outputMint, amount, slippage }) => { try { const quote = await agent.getSwapQuote({ inputMint, outputMint, amount, slippage }); return { content: [{ type: "text", text: JSON.stringify(quote, null, 2) }] }; } catch (error) { return { content: [{ type: "text", text: `Error: ${error.message}` }] }; } }); // Execute Swap server.tool("executeSwap", "Execute a token swap using Jupiter", { quote: z.any(), walletPrivateKey: z.string() }, async ({ quote, walletPrivateKey }) => { try { const result = await agent.executeSwap(quote, walletPrivateKey); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; } catch (error) { return { content: [{ type: "text", text: `Error: ${error.message}` }] }; } }); // Add a dynamic account info resource // Setup specific resources to read from solana.com/docs pages server.resource("solanaDocsInstallation", new ResourceTemplate("solana://docs/intro/installation", { list: undefined }), async (uri) => { try { const response = await fetch(`https://raw.githubusercontent.com/solana-foundation/solana-com/main/content/docs/intro/installation.mdx`); const fileContent = await response.text(); return { contents: [{ uri: uri.href, text: fileContent }] }; } catch (error) { return { contents: [{ uri: uri.href, text: `Error: ${error.message}` }] }; } }); server.resource("solanaDocsClusters", new ResourceTemplate("solana://docs/references/clusters", { list: undefined }), async (uri) => { try { const response = await fetch(`https://raw.githubusercontent.com/solana-foundation/solana-com/main/content/docs/references/clusters.mdx`); const fileContent = await response.text(); return { contents: [{ uri: uri.href, text: fileContent }] }; } catch (error) { return { contents: [{ uri: uri.href, text: `Error: ${error.message}` }] }; } }); server.prompt('calculate-storage-deposit', 'Calculate storage deposit for a specified number of bytes', { bytes: z.string() }, ({ bytes }) => ({ messages: [{ role: 'user', content: { type: 'text', text: `Calculate the SOL amount needed to store ${bytes} bytes of data on Solana using getMinimumBalanceForRentExemption.` } }] })); server.prompt('minimum-amount-of-sol-for-storage', 'Calculate the minimum amount of SOL needed for storing 0 bytes on-chain', () => ({ messages: [{ role: 'user', content: { type: 'text', text: `Calculate the amount of SOL needed to store 0 bytes of data on Solana using getMinimumBalanceForRentExemption & present it to the user as the minimum cost for storing any data on Solana.` } }] })); server.prompt('why-did-my-transaction-fail', 'Look up the given transaction and inspect its logs to figure out why it failed', { signature: z.string() }, ({ signature }) => ({ messages: [{ role: 'user', content: { type: 'text', text: `Look up the transaction with signature ${signature} and inspect its logs to figure out why it failed.` } }] })); server.prompt('how-much-did-this-transaction-cost', 'Fetch the transaction by signature, and break down cost & priority fees', { signature: z.string() }, ({ signature }) => ({ messages: [{ role: 'user', content: { type: 'text', text: `Calculate the network fee for the transaction with signature ${signature} by fetching it and inspecting the 'fee' field in 'meta'. Base fee is 0.000005 sol per signature (also provided as array at the end). So priority fee is fee - (numSignatures * 0.000005). Please provide the base fee and the priority fee.` } }] })); server.prompt('what-happened-in-transaction', 'Look up the given transaction and inspect its logs & instructions to figure out what happened', { signature: z.string() }, ({ signature }) => ({ messages: [{ role: 'user', content: { type: 'text', text: `Look up the transaction with signature ${signature} and inspect its logs & instructions to figure out what happened.` } }] })); server.prompt('account-balance', 'Fetch and analyze all token balances for a Solana wallet address', { walletAddress: z.string() }, ({ walletAddress }) => ({ messages: [{ role: 'user', content: { type: 'text', text: `Get all token balances for the wallet address ${walletAddress}, analyze their USD values, and provide a summary of the wallet's portfolio including total value and distribution of assets.` } }] })); // Start receiving messages on stdin and sending messages on stdout const transport = new StdioServerTransport(); server.connect(transport);