#
tokens: 5959/50000 9/9 files
lines: on (toggle) GitHub
raw markdown copy reset
# 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:
--------------------------------------------------------------------------------

```
1 | dist
2 | node_modules
3 | keypair.json
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Solana MCP Server 🌱
  2 | 
  3 | [![smithery badge](https://smithery.ai/badge/@Grandbusta/solana-mcp)](https://smithery.ai/server/@Grandbusta/solana-mcp)
  4 | 
  5 | A MCP server to interact with the Solana blockchain with your own private key.
  6 | 
  7 | ## 📖 Table of Contents
  8 | - [✨Features](#-features)
  9 | - [⚙️Setup](#️-setup)
 10 | - [Integration with Cursor](#integration-with-cursor)
 11 | - [🛠️Available Tools](#️-available-tools)
 12 | - [🔖License](#️-license)
 13 | 
 14 | ## ✨ Features
 15 | 
 16 | - Get latest slot
 17 | - Get wallet address
 18 | - Get wallet balance
 19 | - Transfer SOL
 20 | 
 21 | ## ⚙️ Setup
 22 | 
 23 | ### Installing via Smithery
 24 | 
 25 | To install Solana MCP for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@Grandbusta/solana-mcp):
 26 | 
 27 | ```bash
 28 | npx -y @smithery/cli install @Grandbusta/solana-mcp --client claude
 29 | ```
 30 | 
 31 | ### Manual Setup
 32 | 
 33 | 1. Clone the repository
 34 | ```bash
 35 | git clone https://github.com/Grandbusta/solana-mcp.git
 36 | ```
 37 | 
 38 | 2. Install dependencies
 39 | ```bash
 40 | npm install
 41 | ```
 42 | 
 43 | 3. Build the project
 44 | ```bash
 45 | npm run build
 46 | ```
 47 | 
 48 | 4. Create a keypair file
 49 | 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.
 50 | 
 51 | 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.
 52 | 
 53 | ## Integration with Cursor
 54 | 
 55 | To integrate with Cursor, follow these steps:
 56 | 
 57 | 1. In the Cursor settings, go to MCP
 58 | 2. Click "Add new MCP server"
 59 | 3. Enter the following information:
 60 |    - Name: Solana MCP
 61 |    - Type: command
 62 |    - Command: ```/path/to/your/solana-mcp/run.sh /path/to/your/keypair.json```
 63 | 
 64 | Example command: ```/Users/username/projects/solana-mcp/run.sh /Users/username/Documents/keypair.json```
 65 | 
 66 | 
 67 | ## 🛠️ Available Tools
 68 | 
 69 | ### 1. get-latest-slot
 70 | Returns the latest slot number:
 71 | 
 72 | ```bash
 73 | 368202671
 74 | ```
 75 | 
 76 | ### 2. get-wallet-address
 77 | Returns the wallet address:
 78 | 
 79 | ```bash
 80 | 5GTuMBag1M8tfe736kcV1vcAE734Zf1SRta8pmWf82TJ
 81 | ```
 82 | 
 83 | ### 3. get-wallet-balance
 84 | Returns the wallet balance in SOL, Lamports, and USD:
 85 | 
 86 | ```bash
 87 | {
 88 |   "lamportsBalance": "4179966000",
 89 |   "solanaBalnce": 4.179966,
 90 |   "usdBalance": "553.0513"
 91 | }
 92 | ```
 93 | 
 94 | ### 4. transfer
 95 | Transfers SOL to a recipient address:
 96 | 
 97 | ```bash
 98 | {
 99 |   "blockTime": "1742316463",
100 |   "meta": {
101 |     "computeUnitsConsumed": "150",
102 |     "err": null,
103 |     "fee": "5000",
104 |     "innerInstructions": [],
105 |     "loadedAddresses": {
106 |       "readonly": [],
107 |       "writable": []
108 |     },
109 |     "logMessages": [
110 |       "Program 11111111111111111111111111111111 invoke [1]",
111 |       "Program 11111111111111111111111111111111 success"
112 |     ],
113 |     "postBalances": [
114 |       "4179966000",
115 |       "819999000",
116 |       "1"
117 |     ],
118 |     "postTokenBalances": [],
119 |     "preBalances": [
120 |       "4399970000",
121 |       "600000000",
122 |       "1"
123 |     ],
124 |     "preTokenBalances": [],
125 |     "rewards": [],
126 |     "status": {
127 |       "Ok": null
128 |     }
129 |   },
130 |   "slot": "368211978",
131 |   "transaction": {
132 |     "message": {
133 |       "accountKeys": [
134 |         "6qhddtBoEHqTc3VM35a3rb3aLUe6vDQfmLigo2G4r5s1",
135 |         "5GTuMBag1M8tfe736kcV1vcAE734Zf1SRta8pmWf82TJ",
136 |         "11111111111111111111111111111111"
137 |       ],
138 |       "addressTableLookups": [],
139 |       "header": {
140 |         "numReadonlySignedAccounts": 0,
141 |         "numReadonlyUnsignedAccounts": 1,
142 |         "numRequiredSignatures": 1
143 |       },
144 |       "instructions": [
145 |         {
146 |           "accounts": [
147 |             0,
148 |             1
149 |           ],
150 |           "data": "3Bxs452Q9hdvHuwd",
151 |           "programIdIndex": 2,
152 |           "stackHeight": null
153 |         }
154 |       ],
155 |       "recentBlockhash": "BLqtPS9BHPp9CRFTrVAsrxFMWC98VTUAQ3vi12bSquLo"
156 |     },
157 |     "signatures": [
158 |       "3bLyqbPn26ofkaxSAVqadQnHqXu9hyoryixmKCn69nunKg2cSryDVAWnfCcYPcGtjSmXcMHfrzc3bw25zFTabXvs"
159 |     ]
160 |   },
161 |   "version": "0"
162 | }
163 | ```
164 | 
165 | 
166 | ## 🧑‍💻 Contributing
167 | 
168 | Contributions are welcome! Please open an issue or submit a pull request.
169 | 
170 | ## 🔖 License
171 | 
172 | [WTFPL License](https://www.wtfpl.net/about/)
173 | 
```

--------------------------------------------------------------------------------
/run.sh:
--------------------------------------------------------------------------------

```bash
 1 | #!/bin/zsh
 2 | 
 3 | if [ -z "$1" ]; then
 4 |     echo "Error: Keypair path argument is required"
 5 |     exit 1
 6 | fi
 7 | 
 8 | export KEYPAIR_PATH=$1
 9 | export RPC_URL="api.devnet.solana.com"
10 | node "$(dirname "$0")/dist/index.js"
```

--------------------------------------------------------------------------------
/example-keypair.json:
--------------------------------------------------------------------------------

```json
1 | [
2 |     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,
3 |     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,
4 |     250, 11, 190, 31, 112, 13, 15, 146, 198, 211, 48, 140, 218, 239
5 | ]
```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "compilerOptions": {
 3 |     "target": "ES2022",
 4 |     "module": "Node16",
 5 |     "moduleResolution": "Node16",
 6 |     "strict": true,
 7 |     "esModuleInterop": true,
 8 |     "skipLibCheck": true,
 9 |     "forceConsistentCasingInFileNames": true,
10 |     "resolveJsonModule": true,
11 |     "outDir": "./dist",
12 |     "rootDir": "./src"
13 |   },
14 |   "include": ["src/**/*"],
15 |   "exclude": ["node_modules"]
16 | }
```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "name": "solana-mcp",
 3 |   "version": "1.0.0",
 4 |   "main": "dist/index.js",
 5 |   "scripts": {
 6 |     "build:start": "npm run build && npm start",
 7 |     "start": "node dist/index.js",
 8 |     "build": "tsc && shx chmod +x dist/index.js",
 9 |     "dev": "tsc --watch",
10 |     "test": "echo \"Error: no test specified\" && exit 1"
11 |   },
12 |   "keywords": [],
13 |   "author": "",
14 |   "license": "ISC",
15 |   "description": "",
16 |   "devDependencies": {
17 |     "@types/node": "^20.8.7",
18 |     "shx": "^0.3.4",
19 |     "typescript": "^5.2.2"
20 |   },
21 |   "dependencies": {
22 |     "@modelcontextprotocol/sdk": "^1.6.1",
23 |     "@solana-program/system": "^0.7.0",
24 |     "@solana/kit": "^2.1.0",
25 |     "zod": "^3.24.2"
26 |   }
27 | }
28 | 
```

--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------

```dockerfile
 1 | # Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile
 2 | FROM node:lts-alpine
 3 | 
 4 | # Create app directory
 5 | WORKDIR /app
 6 | 
 7 | # Copy package files
 8 | COPY package*.json ./
 9 | 
10 | # Install dependencies
11 | RUN npm install --ignore-scripts
12 | 
13 | # Copy rest of the source code
14 | COPY . .
15 | 
16 | # Build the application
17 | RUN npm run build
18 | 
19 | # Set executable permission for run.sh
20 | RUN chmod +x ./run.sh
21 | 
22 | # Expose the application via an entry point. The run.sh requires a keypair argument, which is passed via command-line args.
23 | ENTRYPOINT ["sh", "./run.sh"]
24 | 
25 | # Default command for local testing. This will fail without a keypair argument, so it's expected to be overridden.
26 | CMD ["./example-keypair.json"]
27 | 
```

--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------

```yaml
 1 | # Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml
 2 | 
 3 | startCommand:
 4 |   type: stdio
 5 |   configSchema:
 6 |     # JSON Schema defining the configuration options for the MCP.
 7 |     type: object
 8 |     required:
 9 |       - keypairPath
10 |     properties:
11 |       keypairPath:
12 |         type: string
13 |         description: Absolute or relative path to your keypair JSON file
14 |   commandFunction:
15 |     # A JS function that produces the CLI command based on the given config to start the MCP on stdio.
16 |     |-
17 |     (config) => ({
18 |       command: 'sh',
19 |       args: ['./run.sh', config.keypairPath],
20 |       env: { RPC_URL: process.env.RPC_URL || 'api.devnet.solana.com' }
21 |     })
22 |   exampleConfig:
23 |     keypairPath: ./example-keypair.json
24 | 
```

--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------

```typescript
  1 | #!/usr/bin/env node
  2 | 
  3 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
  4 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
  5 | import { z } from "zod";
  6 | import {
  7 |   createSolanaRpc,
  8 |   address,
  9 |   createSolanaRpcSubscriptions,
 10 |   sendAndConfirmTransactionFactory,
 11 |   pipe,
 12 |   setTransactionMessageFeePayer,
 13 |   createTransactionMessage,
 14 |   createKeyPairSignerFromBytes,
 15 |   setTransactionMessageLifetimeUsingBlockhash,
 16 |   appendTransactionMessageInstruction,
 17 |   KeyPairSigner,
 18 |   signTransactionMessageWithSigners,
 19 |   isSolanaError,
 20 |   SOLANA_ERROR__JSON_RPC__SERVER_ERROR_SEND_TRANSACTION_PREFLIGHT_FAILURE,
 21 |   getSignatureFromTransaction
 22 | } from '@solana/kit'
 23 | import { getTransferSolInstruction } from '@solana-program/system';
 24 | import { readFile } from 'fs/promises'
 25 | import path from "path";
 26 | 
 27 | const solanaRpc = createSolanaRpc(`https://${process.env.RPC_URL}`);
 28 | const solanaRpcSubscription = createSolanaRpcSubscriptions(`wss://${process.env.RPC_URL}`)
 29 | const solanaPriceEndpoint = "https://api.coingecko.com/api/v3/simple/price?ids=solana&vs_currencies=USD"
 30 | const PRICE_CACHE_DURATION = 1 * 60 * 1000
 31 | let cachedPrice: { value: number; timestamp: number } | null = null;
 32 | 
 33 | const sendAndConfirmTransaction = sendAndConfirmTransactionFactory({
 34 |   rpc: solanaRpc,
 35 |   rpcSubscriptions: solanaRpcSubscription,
 36 | })
 37 | 
 38 | 
 39 | function bigIntReplacer(_key: string, value: any): any {
 40 |   return typeof value === 'bigint' ? value.toString() : value;
 41 | }
 42 | 
 43 | function solToLamports(sol: number): number {
 44 |   return sol * 1_000_000_000;
 45 | }
 46 | 
 47 | function lamportsToSol(lamports: number): number {
 48 |   return lamports / 1_000_000_000;
 49 | }
 50 | 
 51 | 
 52 | async function verifyKeypairFile() {
 53 |   if (!process.env.KEYPAIR_PATH) {
 54 |     console.error('Error: KEYPAIR_PATH environment variable is not set');
 55 |     process.exit(1);
 56 |   }
 57 | 
 58 |   const keyPairPath = path.join(process.env.KEYPAIR_PATH as string);
 59 |   try {
 60 |     await readFile(keyPairPath, "utf8");
 61 |   } catch (error: any) {
 62 |     if (error.code === 'ENOENT') {
 63 |       console.error(`Error: Keypair file not found at ${keyPairPath}`);
 64 |     } else if (error.code === 'EACCES') {
 65 |       console.error(`Error: Permission denied reading keypair file at ${keyPairPath}`);
 66 |     } else {
 67 |       console.error(`Error reading keypair file: ${error.message}`);
 68 |     }
 69 |     process.exit(1);
 70 |   }
 71 | }
 72 | 
 73 | async function loadKeypairFromJson() {
 74 |   const keyPairPath = path.join(process.env.KEYPAIR_PATH as string);
 75 |   const keypair = JSON.parse(await readFile(keyPairPath, "utf8"));
 76 |   return keypair;
 77 | }
 78 | 
 79 | 
 80 | async function getSolanaPrice() {
 81 |   try {
 82 |     if (cachedPrice && (Date.now() - cachedPrice.timestamp) < PRICE_CACHE_DURATION) {
 83 |       return cachedPrice.value;
 84 |     }
 85 | 
 86 |     const response = await fetch(solanaPriceEndpoint);
 87 |     const data = await response.json();
 88 | 
 89 |     cachedPrice = {
 90 |       value: data.solana.usd,
 91 |       timestamp: Date.now()
 92 |     };
 93 | 
 94 |     return cachedPrice.value;
 95 |   } catch (error) {
 96 |     throw new Error("Failed to get Solana price");
 97 |   }
 98 | }
 99 | 
100 | async function getSourceAccountSigner() {
101 |   try {
102 |     const SOURCE_ACCOUNT_SIGNER = await createKeyPairSignerFromBytes(
103 |       new Uint8Array(await loadKeypairFromJson())
104 |     )
105 |     return SOURCE_ACCOUNT_SIGNER;
106 |   } catch (error:any) {
107 |     throw new Error(error?.message);
108 |   }
109 | }
110 | 
111 | async function getLatestBlockHash() {
112 |   try {
113 |     const { value: blockHash } = await solanaRpc.getLatestBlockhash().send();
114 |     return blockHash;
115 |   } catch (error:any) {
116 |     throw new Error(error?.message);
117 |   }
118 | }
119 | 
120 | 
121 | 
122 | async function constructTransactionMessage(
123 |   sourceAccountSigner: KeyPairSigner<string>,
124 |   to: string,
125 |   amount: number
126 | ) {
127 |   try {
128 |     const blockHash = await getLatestBlockHash();
129 |     const lamportsAmount = solToLamports(amount);
130 |     const transactionMessage = pipe(
131 |       createTransactionMessage({ version: 0 }),
132 |       (tx: any) => (
133 |         setTransactionMessageFeePayer(sourceAccountSigner.address, tx)
134 |       ),
135 |       (tx: any) => (
136 |         setTransactionMessageLifetimeUsingBlockhash(blockHash, tx)
137 |       ),
138 |       (tx: any) => (
139 |         appendTransactionMessageInstruction(
140 |           getTransferSolInstruction({
141 |             amount: lamportsAmount,
142 |             source: sourceAccountSigner,
143 |             destination: address(to),
144 |           })
145 |           , tx)
146 |       )
147 |     )
148 |     return transactionMessage;
149 |   } catch (error:any) {
150 |     throw new Error(error?.message);
151 |   }
152 | }
153 | 
154 | async function signTransactionMessage(transactionMessage: any) {
155 |   try {
156 |     const signedTransaction = await signTransactionMessageWithSigners(transactionMessage);
157 |     return signedTransaction;
158 |   } catch (error:any) {
159 |     throw new Error(error?.message);
160 |   }
161 | }
162 | 
163 | async function sendTransaction(signedTransaction: any) {
164 |   try {
165 |     const transactionSignature = await sendAndConfirmTransaction(signedTransaction, { commitment: 'confirmed' });
166 |     return transactionSignature;
167 |   } catch (e:any) {
168 |     if (isSolanaError(e, SOLANA_ERROR__JSON_RPC__SERVER_ERROR_SEND_TRANSACTION_PREFLIGHT_FAILURE)) {
169 |       const preflightErrorContext = e.context;
170 |       console.log(preflightErrorContext);
171 |     } else {
172 |       throw e?.message;
173 |     }
174 |   }
175 | 
176 | }
177 | 
178 | 
179 | async function transferTool(args: { to: string, amount: number }) {
180 |   try {
181 |     const sourceAccountSigner = await getSourceAccountSigner()
182 |     const transactionMessage = await constructTransactionMessage(sourceAccountSigner, args.to, args.amount)
183 |     const signedTransaction = await signTransactionMessage(transactionMessage)
184 |     const signature = getSignatureFromTransaction(signedTransaction)
185 |     await sendTransaction(signedTransaction)
186 |     const transaction = await solanaRpc.getTransaction(signature, {
187 |       maxSupportedTransactionVersion: 0
188 |     }).send();
189 |     return transaction;
190 |   } catch (error: any) {
191 |     throw new Error(error?.message);
192 |   }
193 | }
194 | 
195 | async function getSlotTool() {
196 |   try {
197 |     const slot = await solanaRpc.getSlot().send();
198 |     return slot;
199 |   } catch (error:any) {
200 |     throw new Error(error?.message);
201 |   }
202 | }
203 | 
204 | async function getAddressBalanceTool(add: string) {
205 |   try {
206 |     const balance = await solanaRpc.getBalance(address(add)).send();
207 |     return balance.value;
208 |   } catch (error: any) {
209 |     throw new Error(error?.message);
210 |   }
211 | }
212 | 
213 | 
214 | 
215 | // Create an MCP server
216 | const server = new McpServer({
217 |   name: "Solana MCP",
218 |   version: "1.0.0"
219 | });
220 | 
221 | server.tool(
222 |   "get-latest-slot",
223 |   async () => {
224 |     try {
225 |       return {
226 |         content: [{
227 |           type: "text",
228 |           text: String(await getSlotTool())
229 |         }]
230 |       }
231 |     } catch (error:any) {
232 |       return {
233 |         content: [{ type: "text", text: error?.message }],
234 |         isError: true
235 |       }
236 |     }
237 |   }
238 | )
239 | 
240 | 
241 | server.tool(
242 |   "get-wallet-address",
243 |   async () => {
244 |     try {
245 |       let address = (await getSourceAccountSigner()).address as string
246 |       return {
247 |         content: [{
248 |           type: "text",
249 |           text: address
250 |         }]
251 |       }
252 |     } catch (error:any) {
253 |       return {
254 |         content: [{ type: "text", text: `${error?.message}}` }],
255 |         isError: true
256 |       }
257 |     }
258 |   }
259 | )
260 | 
261 | server.tool(
262 |   "get-wallet-balance",
263 |   async () => {
264 |     try {
265 |       let address = (await getSourceAccountSigner()).address as string
266 |       const lamportsBalance = await getAddressBalanceTool(address)
267 |       const solBalance = lamportsToSol(Number(lamportsBalance))
268 |       const price = await getSolanaPrice()
269 |       const usdBalance = (solBalance * price).toFixed(4)
270 |       return {
271 |         content: [{
272 |           type: "text",
273 |           text: JSON.stringify({
274 |             lamportsBalance: lamportsBalance,
275 |             solanaBalnce: solBalance,
276 |             usdBalance: usdBalance
277 |           }, bigIntReplacer, 2)
278 |         }]
279 |       }
280 |     } catch (error:any) {
281 |       return {
282 |         content: [{ type: "text", text: error?.message }],
283 |         isError: true
284 |       }
285 |     }
286 |   }
287 | )
288 | 
289 | server.tool("transfer",
290 |   {
291 |     to: z.string().describe("Recipient wallet address"),
292 |     amount: z.number().describe("Amount in SOL")
293 |   },
294 |   async (args) => {
295 |     try {
296 |       const transaction = await transferTool(args);
297 |       return {
298 |         content: [{ type: "text", text: JSON.stringify(transaction, bigIntReplacer, 2) }]
299 |       }
300 |     } catch (error:any) {
301 |       return {
302 |         content: [{ type: "text", text: error?.message }],
303 |         isError: true
304 |       }
305 |     }
306 |   }
307 | );
308 | 
309 | // Start receiving messages on stdin and sending messages on stdout
310 | const transport = new StdioServerTransport();
311 | 
312 | async function main() {
313 |   await verifyKeypairFile();
314 |   await server.connect(transport);
315 | }
316 | 
317 | main().catch(console.error);
318 | 
```