Model Context Protocol

Connect once. Your agent gets a wallet.

Keyhole is a local MCP server that talks over stdio. Register it with your client and eight wallet tools show up next to the rest.

claude mcp add keyhole \ -e KEYHOLE_PASSPHRASE='your passphrase' \ -e KEYHOLE_RPC=https://api.mainnet-beta.solana.com \ -- keyhole-mcp

Add this to claude_desktop_config.json (Settings → Developer → Edit Config), then restart Claude.

{ "mcpServers": { "keyhole": { "command": "keyhole-mcp", "env": { "KEYHOLE_PASSPHRASE": "your passphrase", "KEYHOLE_RPC": "https://api.mainnet-beta.solana.com" } } } }

Add this to ~/.cursor/mcp.json, or Settings → MCP → Add new server.

{ "mcpServers": { "keyhole": { "command": "keyhole-mcp", "env": { "KEYHOLE_PASSPHRASE": "your passphrase" } } } }

Add this to ~/.codex/config.toml.

[mcp_servers.keyhole] command = "keyhole-mcp" env = { KEYHOLE_PASSPHRASE = "your passphrase" }

Any MCP client that can launch a stdio server works. Run this command and pass the variables below.

keyhole-mcp
Environment

Three variables.

KEYHOLE_PASSPHRASE

Unlocks the vault. Required. Use the one you chose at keyhole init.

KEYHOLE_RPC

Solana RPC URL. Defaults to devnet. Use your own node for the most privacy.

KEYHOLE_HOME

Where the vault lives. Defaults to ~/.keyhole. Handy for running separate vaults per agent.

Tool reference

Eight tools. All of them.

Every wallet argument accepts a label, an id or an address. Errors come back as readable text, so the agent can explain a refusal.

wallet_create

Opens a new wallet. The keypair is generated and encrypted locally. Returns the address to fund. The owner can switch this tool off.

labelstring, max 40optional
{ "label": "research-bot", "address": "7Kq3bXw9RfLm2VtP8cYh4NnAe6JdGsUo1ZiQ5HrkTWpE", "network": "mainnet", "limits": { "maxPerTxSol": 0.1, "dailyLimitSol": 0.5 } }

wallet_send_sol

Sends SOL, plus a 1% Keyhole fee on top on mainnet. Checked in order: frozen, allowlist, per-send cap, rolling 24-hour cap including fees. The amount is reserved before broadcast, so parallel calls can't overshoot. Waits for confirmation before it returns.

walletstring
toaddress
amount_solnumber > 0
memostring, max 200optional
{ "signature": "2uReJSXjSCyvjTrpNpcTYmJKLzbK9xcieJi43AqrJNN63o8DNDB3gNKEcRSCa7zrmewZmwdmB7XZTzRUNEiJwCjW", "sent": 0.05, "keyholeFee": 0.0005, "to": "9xF2pLqTzR4mWv8NcY3hKs6BdJa1GeUo7XiQ5HrnvQ7c" } // refused: Error: 3 SOL is over the per-transaction limit of 0.1 SOL

wallet_send_token

Sends an SPL token, classic or Token-2022, plus a 1% Keyhole fee in the same token on mainnet. Only mints the owner has given a limit can move. Creates the recipient's token account when it doesn't exist yet, and that rent counts against the SOL limit.

walletstring
mintaddress
toaddress
amountnumber, whole tokens
{ "signature": "5Bc1…", "sent": 12.5, "mint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" } // not allowed yet: Error: token EPjF… has no limit set; the owner must allow it with the CLI first

wallet_balance

SOL balance and every token account with a balance above zero.

walletstring
{ "sol": 1.842, "tokens": [ { "mint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", "amount": "250", "decimals": 6 } ] }

wallet_sign_message

Signs plain text with the wallet key, for Sign-In With Solana and similar log-ins. Off until the owner runs keyhole policy <wallet> --sign on. Text with control bytes is refused, so a serialised transaction can't be slipped in as a "message". Every signature is logged.

walletstring
messagestring, 1 to 2000 chars
{ "address": "7Kq3bXw9RfLm2VtP8cYh4NnAe6JdGsUo1ZiQ5HrkTWpE", "message": "Sign in to app.xyz\nNonce: 42", "signature": "3Fx9…" // base58 ed25519 }

wallet_limits

Read-only view of the wallet's caps and what it has sent in the last 24 hours. Useful before the agent plans a payment.

walletstring
{ "frozen": false, "sol": { "maxPerTx": 0.1, "daily": 0.5, "usedLast24hIncludingFees": 0.18 }, "tokens": { "EPjF…": { "maxPerTx": 50, "daily": 200, "usedLast24hBaseUnits": "12500000" } }, "allowlistOnly": false, "signMessages": false }

wallet_history

Recent transactions for the wallet, newest first.

walletstring
limit1 to 50, default 10optional
[ { "signature": "2uRe…", "time": "2026-11-05T09:14:02Z", "ok": true } ]

wallet_list

Labels and addresses of every wallet in this vault. Never returns secrets.

noneno arguments
[ { "label": "research-bot", "address": "7Kq3bXw9RfLm2VtP8cYh4NnAe6JdGsUo1ZiQ5HrkTWpE", "frozen": false } ]

Not exposed over MCP, on purpose

Changing limits, allowing tokens, the allowlist, message signing, freezing, exporting keys and switching off wallet creation. All of these live in the owner CLI and need the passphrase.

Prompt ideas for your agent

"Open a wallet called scout and tell me the address." · "What can you still spend today?" · "Pay 5 USDC to this address for the dataset." · "Sign this log-in message for app.xyz."

Set it up in two minutes.

The docs walk through install, limits and the security model.

Open the docs →