Skip to main content
Glama
liander-ai

safe-solana-mcp

by liander-ai
README.md
# 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

A3.8/5.0

Scored across 7 tools

Disambiguation5/5

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.

Naming Consistency4/5

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.

Tool Count5/5

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.

Completeness4/5

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.

Maintenance

ActivityStale
ResponsivenessNo issues