# Directory Structure ``` ├── .gitignore ├── Dockerfile ├── example-keypair.json ├── package-lock.json ├── package.json ├── README.md ├── run.sh ├── smithery.yaml ├── src │ └── index.ts └── tsconfig.json ``` # Files -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` dist node_modules keypair.json ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown # Solana MCP Server 🌱 [](https://smithery.ai/server/@Grandbusta/solana-mcp) A MCP server to interact with the Solana blockchain with your own private key. ## 📖 Table of Contents - [✨Features](#-features) - [⚙️Setup](#️-setup) - [Integration with Cursor](#integration-with-cursor) - [🛠️Available Tools](#️-available-tools) - [🔖License](#️-license) ## ✨ Features - Get latest slot - Get wallet address - Get wallet balance - Transfer SOL ## ⚙️ Setup ### Installing via Smithery To install Solana MCP for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@Grandbusta/solana-mcp): ```bash npx -y @smithery/cli install @Grandbusta/solana-mcp --client claude ``` ### Manual Setup 1. Clone the repository ```bash git clone https://github.com/Grandbusta/solana-mcp.git ``` 2. Install dependencies ```bash npm install ``` 3. Build the project ```bash npm run build ``` 4. Create a keypair file Create a file named `keypair.json` anywhere you want and copy your private key into it. Check the example-keypair.json file for an example. NB: RPC endpoint is set to `api.devnet.solana.com` by default. If you want to use a different endpoint, you can set it in the `run.sh` file. ## Integration with Cursor To integrate with Cursor, follow these steps: 1. In the Cursor settings, go to MCP 2. Click "Add new MCP server" 3. Enter the following information: - Name: Solana MCP - Type: command - Command: ```/path/to/your/solana-mcp/run.sh /path/to/your/keypair.json``` Example command: ```/Users/username/projects/solana-mcp/run.sh /Users/username/Documents/keypair.json``` ## 🛠️ Available Tools ### 1. get-latest-slot Returns the latest slot number: ```bash 368202671 ``` ### 2. get-wallet-address Returns the wallet address: ```bash 5GTuMBag1M8tfe736kcV1vcAE734Zf1SRta8pmWf82TJ ``` ### 3. get-wallet-balance Returns the wallet balance in SOL, Lamports, and USD: ```bash { "lamportsBalance": "4179966000", "solanaBalnce": 4.179966, "usdBalance": "553.0513" } ``` ### 4. transfer Transfers SOL to a recipient address: ```bash { "blockTime": "1742316463", "meta": { "computeUnitsConsumed": "150", "err": null, "fee": "5000", "innerInstructions": [], "loadedAddresses": { "readonly": [], "writable": [] }, "logMessages": [ "Program 11111111111111111111111111111111 invoke [1]", "Program 11111111111111111111111111111111 success" ], "postBalances": [ "4179966000", "819999000", "1" ], "postTokenBalances": [], "preBalances": [ "4399970000", "600000000", "1" ], "preTokenBalances": [], "rewards": [], "status": { "Ok": null } }, "slot": "368211978", "transaction": { "message": { "accountKeys": [ "6qhddtBoEHqTc3VM35a3rb3aLUe6vDQfmLigo2G4r5s1", "5GTuMBag1M8tfe736kcV1vcAE734Zf1SRta8pmWf82TJ", "11111111111111111111111111111111" ], "addressTableLookups": [], "header": { "numReadonlySignedAccounts": 0, "numReadonlyUnsignedAccounts": 1, "numRequiredSignatures": 1 }, "instructions": [ { "accounts": [ 0, 1 ], "data": "3Bxs452Q9hdvHuwd", "programIdIndex": 2, "stackHeight": null } ], "recentBlockhash": "BLqtPS9BHPp9CRFTrVAsrxFMWC98VTUAQ3vi12bSquLo" }, "signatures": [ "3bLyqbPn26ofkaxSAVqadQnHqXu9hyoryixmKCn69nunKg2cSryDVAWnfCcYPcGtjSmXcMHfrzc3bw25zFTabXvs" ] }, "version": "0" } ``` ## 🧑💻 Contributing Contributions are welcome! Please open an issue or submit a pull request. ## 🔖 License [WTFPL License](https://www.wtfpl.net/about/) ``` -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- ```bash #!/bin/zsh if [ -z "$1" ]; then echo "Error: Keypair path argument is required" exit 1 fi export KEYPAIR_PATH=$1 export RPC_URL="api.devnet.solana.com" node "$(dirname "$0")/dist/index.js" ``` -------------------------------------------------------------------------------- /example-keypair.json: -------------------------------------------------------------------------------- ```json [ 107, 145, 66, 123, 253, 251, 77, 186, 176, 211, 187, 232, 47, 142, 54, 214, 142, 152, 37, 182, 65, 117, 85, 75, 133, 97, 107, 11, 180, 24, 73, 245, 160, 114, 3, 57, 51, 114, 113, 153, 78, 211, 199, 86, 240, 220, 223, 19, 254, 107, 250, 11, 190, 31, 112, 13, 15, 146, 198, 211, 48, 140, 218, 239 ] ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json { "compilerOptions": { "target": "ES2022", "module": "Node16", "moduleResolution": "Node16", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "resolveJsonModule": true, "outDir": "./dist", "rootDir": "./src" }, "include": ["src/**/*"], "exclude": ["node_modules"] } ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json { "name": "solana-mcp", "version": "1.0.0", "main": "dist/index.js", "scripts": { "build:start": "npm run build && npm start", "start": "node dist/index.js", "build": "tsc && shx chmod +x dist/index.js", "dev": "tsc --watch", "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC", "description": "", "devDependencies": { "@types/node": "^20.8.7", "shx": "^0.3.4", "typescript": "^5.2.2" }, "dependencies": { "@modelcontextprotocol/sdk": "^1.6.1", "@solana-program/system": "^0.7.0", "@solana/kit": "^2.1.0", "zod": "^3.24.2" } } ``` -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- ```dockerfile # Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile FROM node:lts-alpine # Create app directory WORKDIR /app # Copy package files COPY package*.json ./ # Install dependencies RUN npm install --ignore-scripts # Copy rest of the source code COPY . . # Build the application RUN npm run build # Set executable permission for run.sh RUN chmod +x ./run.sh # Expose the application via an entry point. The run.sh requires a keypair argument, which is passed via command-line args. ENTRYPOINT ["sh", "./run.sh"] # Default command for local testing. This will fail without a keypair argument, so it's expected to be overridden. CMD ["./example-keypair.json"] ``` -------------------------------------------------------------------------------- /smithery.yaml: -------------------------------------------------------------------------------- ```yaml # Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml startCommand: type: stdio configSchema: # JSON Schema defining the configuration options for the MCP. type: object required: - keypairPath properties: keypairPath: type: string description: Absolute or relative path to your keypair JSON file commandFunction: # A JS function that produces the CLI command based on the given config to start the MCP on stdio. |- (config) => ({ command: 'sh', args: ['./run.sh', config.keypairPath], env: { RPC_URL: process.env.RPC_URL || 'api.devnet.solana.com' } }) exampleConfig: keypairPath: ./example-keypair.json ``` -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- ```typescript #!/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); ```