safe-solana-mcp
# safe-solana-mcp
An MCP server that gives an AI agent **safe, policy-gated access to Solana**.
Most "Solana MCP" servers hand an LLM a raw RPC connection and hope for the best. This one puts a guardrail layer in front: reads are free, but anything that moves value has to pass a policy check and an on-chain simulation first, and even then the server only ever hands back an **unsigned** transaction for you to sign yourself. It holds no keys and never broadcasts.
It speaks the [Model Context Protocol](https://modelcontextprotocol.io), so it drops into Claude Code, Claude Desktop, Cursor, or any MCP client.
## Why
Letting an AI touch a chain is useful (read balances, draft a transfer) and scary (what stops it from draining a wallet?). The answer is not "trust the model." It is a layer the model cannot talk its way around:
- **Devnet by default.** Mainnet is refused unless you explicitly set `ALLOW_MAINNET=true`.
- **Per-transfer cap.** Every transfer is checked against a hard lamport ceiling.
- **Recipient allowlist.** Optionally restrict transfers to a known set of addresses.
- **Simulate before staging.** A transfer is simulated against the cluster; if it would fail, it is never staged.
- **No keys, no broadcast.** The server returns an unsigned transaction. Signing and sending stay with you.
- **Two-step by design.** `prepare_transfer` stages an action; `execute_action` re-checks policy and emits the unsigned transaction. The policy runs at both steps.
The policy check is a single pure function (`src/policy.ts`), so it is easy to audit and covered by tests.
## Tools
| Tool | Kind | What it does |
| --- | --- | --- |
| `get_policy` | read | Show the active cluster and guardrails. |
| `get_balance` | read | SOL balance of an address. |
| `get_account` | read | Owner, lamports, executable flag, and data size of an account. |
| `recent_transactions` | read | Recent transaction signatures for an address. |
| `prepare_transfer` | guarded | Policy-check and simulate a transfer, then stage it. Returns an `actionId`. |
| `execute_action` | guarded | Re-check policy and return the unsigned transaction to sign yourself. |
| `icp_token_balance` | read | Chain Fusion read: an ICRC-1 token's symbol, decimals, and a principal's balance from an Internet Computer ledger canister. |
## Chain Fusion
The same agent surface can look across chains. `icp_token_balance` reads an ICRC-1 token (ckBTC, ckETH, ICP, and so on) straight from its ledger canister on the Internet Computer, no bridge involved, so an agent can compare a Solana balance and an ICP token balance in one place. It is a read-only query call.
## Example session
An agent connected to the server, running with a `0.5` SOL per-transfer cap on devnet:
```text
You ▸ What's the balance of 7QFbRS…QeGgY, and send 5 SOL from it to 9xQeV7…?
Claude ▸ get_balance(7QFbRS…QeGgY)
{ "sol": 1.78, "lamports": 1780000000 }
Claude ▸ prepare_transfer(from 7QFbRS…, to 9xQeV7…, sol 5)
blocked by policy: amount 5000000000 lamports exceeds the
per-transfer cap of 500000000 lamports
Claude ▸ The address holds 1.78 SOL. I did not stage the 5 SOL transfer,
it is over the 0.5 SOL cap this server enforces. Lower the amount,
or raise MAX_SOL_PER_TRANSFER if a larger send is intended.
You ▸ Fine, prepare 0.1 SOL instead.
Claude ▸ prepare_transfer(from 7QFbRS…, to 9xQeV7…, sol 0.1)
{ "actionId": "5f3c…", "simulation": "passed" }
Claude ▸ execute_action(5f3c…)
{ "unsignedTransactionBase64": "AQAB…",
"howToSubmit": "sign with the sender's key and broadcast yourself" }
Claude ▸ Staged and simulated cleanly. Here is the unsigned transaction,
sign it with your wallet and submit it. I never held a key.
```
And a Chain Fusion read on the Internet Computer:
```text
You ▸ How much ckBTC does principal xhk7d… hold?
Claude ▸ icp_token_balance(ledger mxzaz-hqaaa-aaaar-qaada-cai, owner xhk7d…)
{ "symbol": "ckBTC", "decimals": 8, "uiAmount": 0.0123 }
```
## Configuration
All optional. Defaults are safe (devnet, 1 SOL cap, no allowlist, mainnet off).
| Env var | Default | Meaning |
| --- | --- | --- |
| `SOLANA_CLUSTER` | `devnet` | `devnet`, `testnet`, or `mainnet-beta`. |
| `SOLANA_RPC_URL` | cluster default | Custom RPC endpoint. |
| `MAX_SOL_PER_TRANSFER` | `1` | Hard per-transfer cap, in SOL. |
| `ALLOWLIST` | none | Comma-separated recipient addresses. If set, only these may receive. |
| `ALLOW_MAINNET` | `false` | Must be `true` to run on mainnet-beta. |
## Install
```bash
npm install
npm run build
```
Then point your MCP client at the built server. For Claude Desktop / Claude Code, add to the MCP config:
```json
{
"mcpServers": {
"safe-solana": {
"command": "node",
"args": ["/absolute/path/to/safe-solana-mcp/dist/index.js"],
"env": { "SOLANA_CLUSTER": "devnet", "MAX_SOL_PER_TRANSFER": "0.5" }
}
}
}
```
Now you can ask your agent things like *"what's the balance of this address"* or *"prepare a 0.1 SOL transfer to X"* and the guardrails apply automatically.
## Develop
```bash
npm run dev # run the server from source (stdio)
npm run typecheck # tsc, no emit
npm test # policy unit tests (node:test)
```
## Status
Devnet-first reference, unaudited. The transfer path is intentionally unsigned: this server is a guardrail and a transaction builder, not a wallet. Do not point it at mainnet with real funds without reviewing the policy for your use case.
## Stack
TypeScript, the official MCP SDK, `@solana/web3.js`, and `@dfinity/agent` for the Internet Computer read.
## More demos
- **[chain-fusion-solana-wallet](https://github.com/liander-ai/chain-fusion-solana-wallet)** - a Solana wallet owned by an Internet Computer canister (threshold Ed25519, no bridge)
- **[anchor-staking-rewards](https://github.com/liander-ai/anchor-staking-rewards)** - a token staking vault with time-based rewards and a LiteSVM test suite
More at [github.com/liander-ai](https://github.com/liander-ai).
TDQS
Scored across 7 tools
Each tool targets a distinct operation: policy reading, balance checks, account details, transaction preparation/execution, and cross-chain token balance. No two tools overlap in purpose.
Most tools follow a verb_noun pattern (e.g., get_balance, prepare_transfer), but icp_token_balance slightly deviates by leading with a noun prefix. Overall naming is clear and predictable.
With 7 tools, the server is well-scoped for safe Solana operations. The count is neither too few to be useful nor too many to be confusing.
The server covers core functionalities: account queries, balance checks, policy verification, and a secure two-step transfer process. Missing direct send SOL tool, but that's intentionally handled via prepare_transfer/execute_action.