#
tokens: 4399/50000 9/9 files
lines: off (toggle) GitHub
raw markdown copy
# 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 🌱

[![smithery badge](https://smithery.ai/badge/@Grandbusta/solana-mcp)](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);

```