Solana MCP Server

by Grandbusta
Verified
#!/usr/bin/env node import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; import { createSolanaRpc, address, createSolanaRpcSubscriptions, sendAndConfirmTransactionFactory, pipe, setTransactionMessageFeePayer, createTransactionMessage, createKeyPairSignerFromBytes, setTransactionMessageLifetimeUsingBlockhash, appendTransactionMessageInstruction, KeyPairSigner, signTransactionMessageWithSigners, isSolanaError, SOLANA_ERROR__JSON_RPC__SERVER_ERROR_SEND_TRANSACTION_PREFLIGHT_FAILURE, getSignatureFromTransaction } from '@solana/kit' import { getTransferSolInstruction } from '@solana-program/system'; import { readFile } from 'fs/promises' import path from "path"; const solanaRpc = createSolanaRpc(`https://${process.env.RPC_URL}`); const solanaRpcSubscription = createSolanaRpcSubscriptions(`wss://${process.env.RPC_URL}`) const solanaPriceEndpoint = "https://api.coingecko.com/api/v3/simple/price?ids=solana&vs_currencies=USD" const PRICE_CACHE_DURATION = 1 * 60 * 1000 let cachedPrice: { value: number; timestamp: number } | null = null; const sendAndConfirmTransaction = sendAndConfirmTransactionFactory({ rpc: solanaRpc, rpcSubscriptions: solanaRpcSubscription, }) function bigIntReplacer(_key: string, value: any): any { return typeof value === 'bigint' ? value.toString() : value; } function solToLamports(sol: number): number { return sol * 1_000_000_000; } function lamportsToSol(lamports: number): number { return lamports / 1_000_000_000; } async function verifyKeypairFile() { if (!process.env.KEYPAIR_PATH) { console.error('Error: KEYPAIR_PATH environment variable is not set'); process.exit(1); } const keyPairPath = path.join(process.env.KEYPAIR_PATH as string); try { await readFile(keyPairPath, "utf8"); } catch (error: any) { if (error.code === 'ENOENT') { console.error(`Error: Keypair file not found at ${keyPairPath}`); } else if (error.code === 'EACCES') { console.error(`Error: Permission denied reading keypair file at ${keyPairPath}`); } else { console.error(`Error reading keypair file: ${error.message}`); } process.exit(1); } } async function loadKeypairFromJson() { const keyPairPath = path.join(process.env.KEYPAIR_PATH as string); const keypair = JSON.parse(await readFile(keyPairPath, "utf8")); return keypair; } async function getSolanaPrice() { try { if (cachedPrice && (Date.now() - cachedPrice.timestamp) < PRICE_CACHE_DURATION) { return cachedPrice.value; } const response = await fetch(solanaPriceEndpoint); const data = await response.json(); cachedPrice = { value: data.solana.usd, timestamp: Date.now() }; return cachedPrice.value; } catch (error) { throw new Error("Failed to get Solana price"); } } async function getSourceAccountSigner() { try { const SOURCE_ACCOUNT_SIGNER = await createKeyPairSignerFromBytes( new Uint8Array(await loadKeypairFromJson()) ) return SOURCE_ACCOUNT_SIGNER; } catch (error:any) { throw new Error(error?.message); } } async function getLatestBlockHash() { try { const { value: blockHash } = await solanaRpc.getLatestBlockhash().send(); return blockHash; } catch (error:any) { throw new Error(error?.message); } } async function constructTransactionMessage( sourceAccountSigner: KeyPairSigner<string>, to: string, amount: number ) { try { const blockHash = await getLatestBlockHash(); const lamportsAmount = solToLamports(amount); const transactionMessage = pipe( createTransactionMessage({ version: 0 }), (tx: any) => ( setTransactionMessageFeePayer(sourceAccountSigner.address, tx) ), (tx: any) => ( setTransactionMessageLifetimeUsingBlockhash(blockHash, tx) ), (tx: any) => ( appendTransactionMessageInstruction( getTransferSolInstruction({ amount: lamportsAmount, source: sourceAccountSigner, destination: address(to), }) , tx) ) ) return transactionMessage; } catch (error:any) { throw new Error(error?.message); } } async function signTransactionMessage(transactionMessage: any) { try { const signedTransaction = await signTransactionMessageWithSigners(transactionMessage); return signedTransaction; } catch (error:any) { throw new Error(error?.message); } } async function sendTransaction(signedTransaction: any) { try { const transactionSignature = await sendAndConfirmTransaction(signedTransaction, { commitment: 'confirmed' }); return transactionSignature; } catch (e:any) { if (isSolanaError(e, SOLANA_ERROR__JSON_RPC__SERVER_ERROR_SEND_TRANSACTION_PREFLIGHT_FAILURE)) { const preflightErrorContext = e.context; console.log(preflightErrorContext); } else { throw e?.message; } } } async function transferTool(args: { to: string, amount: number }) { try { const sourceAccountSigner = await getSourceAccountSigner() const transactionMessage = await constructTransactionMessage(sourceAccountSigner, args.to, args.amount) const signedTransaction = await signTransactionMessage(transactionMessage) const signature = getSignatureFromTransaction(signedTransaction) await sendTransaction(signedTransaction) const transaction = await solanaRpc.getTransaction(signature, { maxSupportedTransactionVersion: 0 }).send(); return transaction; } catch (error: any) { throw new Error(error?.message); } } async function getSlotTool() { try { const slot = await solanaRpc.getSlot().send(); return slot; } catch (error:any) { throw new Error(error?.message); } } async function getAddressBalanceTool(add: string) { try { const balance = await solanaRpc.getBalance(address(add)).send(); return balance.value; } catch (error: any) { throw new Error(error?.message); } } // Create an MCP server const server = new McpServer({ name: "Solana MCP", version: "1.0.0" }); server.tool( "get-latest-slot", async () => { try { return { content: [{ type: "text", text: String(await getSlotTool()) }] } } catch (error:any) { return { content: [{ type: "text", text: error?.message }], isError: true } } } ) server.tool( "get-wallet-address", async () => { try { let address = (await getSourceAccountSigner()).address as string return { content: [{ type: "text", text: address }] } } catch (error:any) { return { content: [{ type: "text", text: `${error?.message}}` }], isError: true } } } ) server.tool( "get-wallet-balance", async () => { try { let address = (await getSourceAccountSigner()).address as string const lamportsBalance = await getAddressBalanceTool(address) const solBalance = lamportsToSol(Number(lamportsBalance)) const price = await getSolanaPrice() const usdBalance = (solBalance * price).toFixed(4) return { content: [{ type: "text", text: JSON.stringify({ lamportsBalance: lamportsBalance, solanaBalnce: solBalance, usdBalance: usdBalance }, bigIntReplacer, 2) }] } } catch (error:any) { return { content: [{ type: "text", text: error?.message }], isError: true } } } ) server.tool("transfer", { to: z.string().describe("Recipient wallet address"), amount: z.number().describe("Amount in SOL") }, async (args) => { try { const transaction = await transferTool(args); return { content: [{ type: "text", text: JSON.stringify(transaction, bigIntReplacer, 2) }] } } catch (error:any) { return { content: [{ type: "text", text: error?.message }], isError: true } } } ); // Start receiving messages on stdin and sending messages on stdout const transport = new StdioServerTransport(); async function main() { await verifyKeypairFile(); await server.connect(transport); } main().catch(console.error);